Files
avcf/blocks-algo.js
T
adrien 439cc6e7ba feat: loop support via goto labels on each block
- Every block now emits a C label (its id) before its code
- generateNextCode emits goto label; for back-edges (visited targets)
- _emitBranch delegates to generateCode (not generateOwnCode), so labels
  are emitted consistently in both main and branch traversals
- generateCallWithError threads a shared visited set across all three
  emitBranch calls, preventing duplicate label emission for shared blocks
- emitBranch skips roots already visited by previous roots in the same
  branch (prevents spurious gotos from shared-block traversal)
2026-06-23 01:16:14 +02:00

558 lines
19 KiB
JavaScript

// ── Algorithm-tab block definitions ────────────────────────────────────────
//
// Blocks that live on the Algorithm (flowchart) canvas:
// WCStart, WCEnd, WCOutput, WCDecision, WCAssign, WCExpression, WCVarRef
//
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
//
// Load order dependencies:
// wcblock.js → WCBlock base class
// helpers.js → findDataSource, resolveSourceName, escapeString, toCName,
// commentPrefix, refreshVarRefOptions
// dom.js → $id
// error.js → showError
// ── WCStart — program entry point ──────────────────────────────────────────
/** Program entry point. Cannot be deleted. */
class WCStart extends WCBlock {
delete() { showError("Cannot delete the start block."); }
}
// ── WCEnd — program exit ───────────────────────────────────────────────────
/** Program exit block with errno autocomplete. At least one must return 0. */
class WCEnd extends WCBlock {
connectedCallback() {
super.connectedCallback();
this._errnoItems = _ERRNO_ITEMS;
this._selectedCode = 0;
this._el.input = this._require(".end-errno-input");
this._el.suggestions = this._require(".end-errno-suggestions");
this._el.desc = this._require(".end-desc-text");
this._el.input.addEventListener("input", () => this._onInput());
this._el.input.addEventListener("focus", () => { if (this._el.suggestions.children.length > 0) this._showSuggestions(); });
this._el.input.addEventListener("keydown", ev => this._onKeyDown(ev));
this._el.suggestions.addEventListener("mousedown", ev => {
const sug = ev.target.closest(".end-errno-suggestion");
if (sug) {
const code = parseInt(sug.dataset.code);
if (!isNaN(code)) this._selectByCode(code);
}
});
document.addEventListener("click", ev => {
if (!this.contains(ev.target)) this._hideSuggestions();
});
this._el.input.value = "0";
this._selectByCode(0);
}
_onInput() {
const q = this._el.input.value.trim().toLowerCase();
if (!q) {
this._renderSuggestions(this._errnoItems);
this._showSuggestions();
return;
}
const qCode = parseInt(q);
const matches = this._errnoItems.filter(item =>
(!isNaN(qCode) && item.code === qCode) ||
item.name.toLowerCase().includes(q) ||
item.desc.toLowerCase().includes(q)
);
this._renderSuggestions(matches);
this._showSuggestions();
}
_renderSuggestions(items) {
this._el.suggestions.innerHTML = "";
for (const item of items) this._addSuggestion(item);
}
_addSuggestion(item) {
const div = document.createElement("div");
div.className = "end-errno-suggestion";
div.dataset.code = item.code;
const codeSpan = document.createElement("span");
codeSpan.className = "end-suggestion-code";
codeSpan.textContent = item.code + " " + item.name;
div.appendChild(codeSpan);
const descSpan = document.createElement("span");
descSpan.className = "end-suggestion-desc";
descSpan.textContent = item.desc;
div.appendChild(descSpan);
this._el.suggestions.appendChild(div);
}
_showSuggestions() { this._el.suggestions.style.display = "block"; }
_hideSuggestions() { this._el.suggestions.style.display = "none"; }
_onKeyDown(ev) {
if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
ev.preventDefault();
const items = [...this._el.suggestions.querySelectorAll(".end-errno-suggestion")];
let idx = items.findIndex(el => el.classList.contains("highlighted"));
if (ev.key === "ArrowDown") idx = Math.min(idx + 1, items.length - 1);
else idx = Math.max(idx - 1, 0);
items.forEach(el => el.classList.remove("highlighted"));
if (items[idx]) {
items[idx].classList.add("highlighted");
items[idx].scrollIntoView({ block: "nearest" });
}
}
if (ev.key === "Enter") {
const hl = this._el.suggestions.querySelector(".highlighted");
if (hl) {
ev.preventDefault();
this._selectByCode(parseInt(hl.dataset.code));
}
}
if (ev.key === "Escape") this._hideSuggestions();
}
_selectByCode(code) {
this._selectedCode = code;
const item = this._errnoItems.find(i => i.code === code);
if (item) {
const display = code + " " + item.name;
this._el.input.value = display;
this._el.input.setAttribute("value", display);
this._el.desc.textContent = item.desc;
this.dataset.exitError = code !== 0 ? "true" : "";
} else {
const display = code + " error";
this._el.input.value = display;
this._el.input.setAttribute("value", display);
this._el.desc.textContent = "unknown error";
this.dataset.exitError = "true";
}
this._hideSuggestions();
}
_enforceZeroEnd() {
if (this._selectedCode === 0) return;
const others = document.querySelectorAll("wc-end");
for (const other of others) {
if (other !== this && other._selectedCode === 0) return;
}
this._selectByCode(0);
}
_hasOtherZero() {
const others = document.querySelectorAll("wc-end");
for (const other of others) {
if (other !== this && other._selectedCode === 0) return true;
}
return false;
}
delete() {
if (this.id === "main-end") { showError("Cannot delete the default end block."); return; }
if (!this._hasOtherZero()) { showError("At least one end block must return 0."); return; }
super.delete();
setTimeout(() => {
for (const end of document.querySelectorAll("wc-end")) end._enforceZeroEnd();
}, 0);
}
_genStringFrees() {
const freed = new Set();
let code = "";
for (const el of document.querySelectorAll("#variable-area > [data-block]")) {
if (el.dataType === "string") {
const name = resolveSourceName(el);
if (name && !freed.has(name)) {
freed.add(name);
code += ` free(${name}.data);\n`;
}
}
}
return code;
}
generateOwnCode() {
const code = this._selectedCode;
if (code === 0) {
const frees = this._genStringFrees();
if (frees) return `\n${frees} return 0;\n`;
return "\n return 0;\n";
}
return `\n exit(${code});\n`;
}
saveData() { return { exitCode: this._selectedCode }; }
restoreData(d) {
this._selectByCode(d.exitCode !== undefined ? d.exitCode : 0);
}
}
function _buildErrnoItems() {
const items = [
{ code: 0, name: "success", desc: "success" },
{ code: 255, name: "error", desc: "generic error" },
];
for (const e of ERRNO_LIST) {
items.push({ code: e.code, name: e.name, desc: e.desc });
}
return items;
}
const _ERRNO_ITEMS = _buildErrnoItems();
// ── WCOutput — printf statement ────────────────────────────────────────────
/** Output description block. Provides inline documentation without generating code. */
class WCOutput extends WCBlock {
connectedCallback() {
super.connectedCallback();
this._el.textarea = this.querySelector("textarea");
}
saveData() { return { textarea: this._el.textarea.value }; }
restoreData(d) { this.querySelector("textarea").value = d.textarea || ""; }
generateOwnCode() {
const msg = escapeString(this._el.textarea.value || "");
return `\nprintf("${msg}");\n`;
}
}
// ── WCDecision — conditional branch ────────────────────────────────────────
/** Conditional branch block; splits flow into yes/no branches and emits if/else C code. */
class WCDecision extends WCBlock {
connectedCallback() {
super.connectedCallback();
this._el.boolIn = this.querySelector(".decision-data-in");
}
_getBoolName(block) {
return resolveSourceName(block) || "flag";
}
_branchDescendants(roots) {
const d = new Set(), q = [...roots];
for (const el of q) {
if (d.has(el)) continue;
d.add(el);
if (el instanceof WCDecision) continue;
for (const p of el.wcNextFlow) {
if (!d.has(p.toElement)) q.push(p.toElement);
}
}
return d;
}
_emitBranch(roots, stop, visited) {
let c = "";
const walk = el => {
if (stop && stop.has(el)) return;
if (visited.has(el)) {
c += "\n goto " + el.id + ";\n";
return;
}
c += el.generateCode(visited);
};
for (const el of roots) {
if (!visited.has(el)) walk(el);
}
return c;
}
generateCode(visited = new Set()) {
if (visited.has(this)) return "\n goto " + this.id + ";\n";
visited.add(this);
const header = "\n" + this.id + ":\n";
const yesTargets = [], noTargets = [];
for (const path of this.wcNextFlow) {
const hook = path.fromHook;
if (!hook || hook.id === "hook-out1") {
yesTargets.push(path.toElement);
} else if (hook.id === "hook-out0") {
noTargets.push(path.toElement);
}
}
const yesAll = this._branchDescendants(yesTargets);
const noAll = this._branchDescendants(noTargets);
const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this));
const yesCode = this._emitBranch(yesTargets, shared, visited);
const noCode = this._emitBranch(noTargets, shared, visited);
const sharedCode = this._emitBranch([...shared], new Set(), visited);
for (const el of yesAll) visited.add(el);
for (const el of noAll) visited.add(el);
const source = findDataSource(this._el.boolIn);
const cond = source ? this._getBoolName(source) : "0";
let out = "";
if (yesCode || noCode) {
if (yesCode) out += `if (${cond}) {\n${yesCode}}`;
if (noCode) {
out += yesCode
? ` else {\n${noCode}}`
: `if (!(${cond})) {\n${noCode}}`;
}
}
out += sharedCode;
return out ? header + out + "\n" : header;
}
}
// ── WCAssign — variable assignment block ───────────────────────────────────
/** Variable assignment block; writes to scalar/array/vector/string targets with data hooks. */
class WCAssign extends WCBlock {
connectedCallback() {
super.connectedCallback();
this._el.target = this.querySelector(".assign-target");
this._el.value = this.querySelector(".assign-value");
this._el.dataIn = this.querySelector(".assign-row .hook");
this._el.indexArea = this.querySelector(".assign-index-area");
this._el.indexInput = this.querySelector(".assign-index");
this._el.indexHook = this.querySelector(".assign-index-area .hook");
this._el.target.addEventListener("change", () => this._syncType());
setTimeout(() => {
refreshVarRefOptions();
this._syncType();
this._updateValueVisibility();
}, 0);
}
_updateValueVisibility() {
const hasConn = findDataSource(this._el.dataIn) !== null;
this._el.value.classList.toggle("hidden", hasConn);
const hasIdxConn = findDataSource(this._el.indexHook) !== null;
this._el.indexInput.classList.toggle("hidden", hasIdxConn);
}
_syncType() {
const id = this._el.target.value;
const target = id ? $id(id) : null;
const type = target ? target.dataType : "scalar";
this._el.dataIn.dataset.type = type;
this._el.dataIn.title = "expects " + type;
this._el.indexArea.style.display = type === "array" ? "flex" : "none";
}
_getSourceVarName(block) {
return resolveSourceName(block) || "x";
}
_genStringLiteral(name, raw) {
const escaped = escapeString(raw);
const len = raw.length;
return "\n" + [
`free(${name}.data);`,
`${name}.length = ${len};`,
`${name}.data = malloc(${len} + 1);`,
`memcpy(${name}.data, "${escaped}", ${len});`,
`${name}.data[${len}] = 0;`,
].join("\n") + "\n";
}
_genStringCopy(dst, src) {
return "\n" + [
`free(${dst}.data);`,
`${dst}.length = ${src}.length;`,
`${dst}.data = malloc(${src}.length + 1);`,
`memcpy(${dst}.data, ${src}.data, ${src}.length);`,
`${dst}.data[${dst}.length] = 0;`,
].join("\n") + "\n";
}
saveData() { return { target: this._el.target.value, value: this._el.value.value, index: this._el.indexInput.value }; }
restoreData(d) {
const tgt = this.querySelector(".assign-target");
tgt.dataset.restoredValue = d.target || "";
tgt.value = d.target || "";
this.querySelector(".assign-value").value = d.value || "";
this.querySelector(".assign-index").value = d.index || "0";
this._syncType();
this._updateValueVisibility();
}
generateOwnCode() {
const targetId = this._el.target.value;
if (!targetId) return "";
const target = $id(targetId);
if (!target) return "";
const targetName = resolveSourceName(target) || "x";
const targetType = target.dataType || "scalar";
const source = findDataSource(this._el.dataIn);
if (targetType === "array") {
const idxSource = findDataSource(this._el.indexHook);
const idxExpr = idxSource ? this._getSourceVarName(idxSource) : (this._el.indexInput.value || "0");
if (!source) {
const raw = this._el.value.value || "0";
return `\n${targetName}[${idxExpr}] = ${raw};\n`;
}
const srcName = this._getSourceVarName(source);
return `\n${targetName}[${idxExpr}] = ${srcName};\n`;
}
if (!source) {
const raw = this._el.value.value || "0";
if (targetType === "string") return this._genStringLiteral(targetName, raw);
return `\n${targetName} = ${raw};\n`;
}
const srcName = this._getSourceVarName(source);
if (targetType === "string" && source.dataType === "string") {
return this._genStringCopy(targetName, srcName);
}
return `\n${targetName} = ${srcName};\n`;
}
}
// ── WCVarRef — read-only variable reference with data output hook ──────────
/** Read-only variable reference with data output hook; syncs type from selected variable. */
class WCVarRef extends WCBlock {
connectedCallback() {
super.connectedCallback();
this._el.select = this.querySelector(".varref-select");
this._el.select.addEventListener("change", () => {
this._syncType();
});
}
_syncType() {
const id = this._el.select.value;
const target = id ? $id(id) : null;
const hook = this.querySelector(".hook-out");
hook.dataset.type = target ? target.dataType : "scalar";
}
saveData() { return { selected: this._el.select.value }; }
restoreData(d) {
const sel = this.querySelector(".varref-select");
sel.dataset.restoredValue = d.selected || "";
sel.value = d.selected || "";
const id = sel.value;
const target = id ? $id(id) : null;
const hook = this.querySelector(".hook-out");
hook.dataset.type = target ? target.dataType : "scalar";
}
generateOwnCode() { return ""; }
}
// ── WCExpression — mathematical expression with MathJax rendering & data hooks ──
/** Math expression block with LaTeX rendering via MathJax and dynamic variable data hooks. */
class WCExpression extends WCBlock {
get dataType() {
const el = this._el?.latex || this.querySelector(".expr-latex");
const latex = el?.value || "";
return /[<>]|==|!=|\\(?:ge|le|ne|neq|geq|leq|land|lor|wedge|vee|neg|lnot)\b/.test(latex) ? "boolean" : "scalar";
}
connectedCallback() {
super.connectedCallback();
this._el.name = this.querySelector(".expr-name");
this._el.latex = this.querySelector(".expr-latex");
this._el.rendered = this.querySelector(".expr-rendered");
this._el.varhooks = this.querySelector(".expr-varhooks");
this._el.latex.addEventListener("input", () => this._renderFormula());
setTimeout(() => this._renderFormula(), 0);
}
saveData() { return { name: this._el.name.value, latex: this._el.latex.value }; }
restoreData(d) {
this._el.name.value = d.name || "";
this._el.latex.value = d.latex || "";
this._renderFormula();
}
_parseVariables(latex) {
const tokens = latex
.replace(/\\[a-zA-Z]+/g, "")
.replace(/[{}\[\]()]/g, " ")
.replace(/[\d+*\/^_=,;:!?<>|~`@#\$%&'-]/g, " ")
.split(/\s+/)
.filter(t => t.length > 0 && t.length <= 8)
.filter(t => ![
"sin","cos","tan","cot","sec","csc","arcsin","arccos","arctan",
"log","ln","lg","exp","sqrt","lim","inf","sum","int","prod","det",
"gcd","lcm","max","min","arg","mod","Re","Im","deg","Pr","var",
"alpha","beta","gamma","delta","epsilon","zeta","eta","theta",
"iota","kappa","lambda","mu","nu","xi","omicron","rho","sigma",
"tau","upsilon","phi","chi","psi","omega","varepsilon","vartheta",
"varpi","varphi","varrho","varsigma","varUpsilon"
].includes(t.toLowerCase()));
return [...new Set(tokens)];
}
_rebuildVarHooks(vars) {
this._el.varhooks.innerHTML = "";
for (const v of vars) {
const hook = document.createElement("button");
hook.className = "hook";
hook.id = this.id + "-vh-" + v;
hook.title = v;
hook.textContent = v;
hook.dataset.direction = "in";
hook.dataset.linkType = "data";
hook.dataset.type = "scalar";
hook.dataset.exit = "left";
this._setupHook(hook);
this._el.varhooks.appendChild(hook);
}
}
_renderFormula() {
const latex = this._el.latex.value;
const outHook = this.querySelector(".hook-out");
if (outHook) outHook.dataset.type = this.dataType;
this._el.rendered.innerHTML = "";
const vars = latex.trim() ? this._parseVariables(latex) : [];
this._rebuildVarHooks(vars);
if (!latex.trim()) return;
if (window.MathJax && MathJax.typesetPromise) {
const wrapper = document.createElement("div");
wrapper.textContent = "$$" + latex + "$$";
const typeset = () => {
MathJax.typesetPromise([wrapper]).then(() => {
this._el.rendered.innerHTML = wrapper.innerHTML;
}).catch(() => {
this._el.rendered.textContent = "\u26A0 error";
});
};
if (MathJax.startup && MathJax.startup.promise && MathJax.startup.promise.then) {
MathJax.startup.promise.then(typeset);
} else {
typeset();
}
} else {
this._el.rendered.textContent = latex;
}
}
generateCode(visited = new Set()) {
if (visited.has(this)) return "";
visited.add(this);
return this.generateOwnCode();
}
generateOwnCode() {
const name = this._el.name.value || "res";
const formula = this._el.latex.value || "0";
const type = this.dataType === "boolean" ? "int" : "double";
return `${type} ${name} = ${formula};`;
}
}