refactor: unwrap nested if-else in error-path code generation

- Replace if/else wrapping in WCProcCall.generateOwnCode with branch-aware
  generation via _branchDescendants, _emitBranch, and _generateCallWithError
- Shared blocks (reachable from both error and success paths) now appear
  only once after the if-block, avoiding deeply nested code
- Add 8 browser tests covering merge, decision passthrough, value style,
  sequential calls, and shared-only error paths
- Increase template block spacing (16px -> 40px) for readability
- Add error WCEnd blocks to console and SDL3 templates with error-hook
  connections to all failure-mode proc calls
- Hide editable WCProcCall input fields behind field-display spans,
  matching existing algorithm-block convention
This commit is contained in:
2026-06-23 00:51:54 +02:00
parent 5caa306503
commit 68da5592ef
3 changed files with 331 additions and 21 deletions
+83 -16
View File
@@ -167,6 +167,52 @@ function _specDataType(spec) {
return "int"; return "int";
} }
/**
* Collects all blocks reachable from the given root blocks via wcNextFlow,
* excluding the interior of decision blocks (decisions handle their own branching).
* Used by WCProcCall._generateCallWithError to compute shared vs exclusive branches.
* @param {HTMLElement[]} roots
* @returns {Set<HTMLElement>}
*/
function _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;
}
/**
* Emits C code for a branch by walking from root blocks, stopping at shared blocks
* and decision blocks (which emit their own branching). Uses visited sets to prevent
* double-emission when shared code is later emitted separately.
* @param {HTMLElement[]} roots
* @param {Set<HTMLElement>} stop blocks to exclude (shared between branches)
* @param {Set<HTMLElement>} visited blocks already emitted
* @returns {string}
*/
function _emitBranch(roots, stop, visited) {
const v = new Set(visited);
let c = "";
const walk = el => {
if (v.has(el) || stop.has(el)) return;
v.add(el);
if (el instanceof WCDecision) {
c += el.generateCode(v);
} else {
c += el.generateOwnCode();
for (const p of el.wcNextFlow) walk(p.toElement);
}
};
for (const el of roots) walk(el);
return c;
}
/** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */ /** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */
class WCProcCall extends WCBlock { class WCProcCall extends WCBlock {
connectedCallback() { connectedCallback() {
@@ -379,6 +425,7 @@ class WCProcCall extends WCBlock {
row.appendChild(input); row.appendChild(input);
this._el.fmtargsList.appendChild(row); this._el.fmtargsList.appendChild(row);
this._setupHook(hook); this._setupHook(hook);
this._wrapArgInput(input);
} }
_addFormatArg() { _addFormatArg() {
@@ -409,6 +456,7 @@ class WCProcCall extends WCBlock {
row.appendChild(input); row.appendChild(input);
this._el.fmtargsList.appendChild(row); this._el.fmtargsList.appendChild(row);
this._setupHook(hook); this._setupHook(hook);
this._wrapArgInput(input);
this._ensureFmtUnconnected(); this._ensureFmtUnconnected();
} }
@@ -419,6 +467,18 @@ class WCProcCall extends WCBlock {
if (!hasFree) this._addFormatArg(); if (!hasFree) this._addFormatArg();
} }
_wrapArgInput(input) {
if (input.style.display === "none") return;
const display = document.createElement("span");
display.className = "field-display";
const update = () => { display.textContent = input.value || input.placeholder || ""; };
update();
input.addEventListener("change", update);
input.addEventListener("input", update);
input.parentNode.insertBefore(display, input);
input.style.display = "none";
}
_createArgField(container, param, dir) { _createArgField(container, param, dir) {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "proccall-arg-row"; row.className = "proccall-arg-row";
@@ -454,6 +514,7 @@ class WCProcCall extends WCBlock {
container.appendChild(row); container.appendChild(row);
this._setupHook(hook); this._setupHook(hook);
this._wrapArgInput(input);
} }
_sourceName(block) { _sourceName(block) {
@@ -522,14 +583,7 @@ class WCProcCall extends WCBlock {
if (returnStyle === "error") { if (returnStyle === "error") {
const call = `${procName}(${inputs.join(", ")})`; const call = `${procName}(${inputs.join(", ")})`;
if (hasErrPath) { if (hasErrPath) {
const errCode = errPaths[0].toElement.generateCode(new Set()); return this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
let okCode = "";
for (const p of this.wcNextFlow) {
if (p.fromHook !== this._el.errHook) {
okCode += p.toElement.generateCode(new Set());
}
}
return `\nif (${call} != 0) {\n${errCode}} else {\n${okCode}}\n`;
} }
return `\n${call};\n`; return `\n${call};\n`;
} }
@@ -537,14 +591,7 @@ class WCProcCall extends WCBlock {
if (returnStyle === "value" && outputs.length === 1) { if (returnStyle === "value" && outputs.length === 1) {
const outVar = outputs[0].replace(/^&/, ""); const outVar = outputs[0].replace(/^&/, "");
if (hasErrPath) { if (hasErrPath) {
const errCode = errPaths[0].toElement.generateCode(new Set()); return this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
let okCode = "";
for (const p of this.wcNextFlow) {
if (p.fromHook !== this._el.errHook) {
okCode += p.toElement.generateCode(new Set());
}
}
return `\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0) {\n${errCode}} else {\n${okCode}}\n`;
} }
return `\n${procName}(${inputs.join(", ")});\n`; return `\n${procName}(${inputs.join(", ")});\n`;
} }
@@ -552,6 +599,26 @@ class WCProcCall extends WCBlock {
return `\n${procName}(${args});\n`; return `\n${procName}(${args});\n`;
} }
_generateCallWithError(ifHeader, errPaths) {
const errTargets = errPaths.map(p => p.toElement);
const okTargets = Array.from(this.wcNextFlow)
.filter(p => p.fromHook !== this._el.errHook)
.map(p => p.toElement);
const errAll = _branchDescendants(errTargets);
const okAll = _branchDescendants(okTargets);
const shared = new Set([...errAll].filter(el => okAll.has(el)));
const errCode = _emitBranch(errTargets, shared, new Set());
const okCode = _emitBranch(okTargets, shared, new Set());
const sharedCode = _emitBranch([...shared], new Set(), new Set());
let out = `${ifHeader} {\n${errCode}}\n`;
if (okCode) out += okCode;
if (sharedCode) out += sharedCode;
return out;
}
saveData() { saveData() {
const args = {}; const args = {};
for (const inp of this.querySelectorAll(".proccall-arg-input")) { for (const inp of this.querySelectorAll(".proccall-arg-input")) {
+23 -5
View File
@@ -50,7 +50,8 @@ function _pos(el, left, top) {
el.style.top = top + "px"; el.style.top = top + "px";
} }
function _placeBelow(prev, block, gap = 16) { const ALG_GAP = 40;
function _placeBelow(prev, block, gap = ALG_GAP) {
block.style.top = (prev.offsetTop + prev.offsetHeight + gap) + "px"; block.style.top = (prev.offsetTop + prev.offsetHeight + gap) + "px";
} }
@@ -133,11 +134,17 @@ TEMPLATES.console = {
if (arg) arg.value = '"Hello, World!\\n"'; if (arg) arg.value = '"Hello, World!\\n"';
} }
_pos(end, 50, printfCall.offsetTop + printfCall.offsetHeight + 16); const errEnd = prog.newNode("wc-end");
_pos(errEnd, 360, start.offsetTop);
errEnd._selectByCode(1);
errEnd._enforceZeroEnd();
if (start && end && printfCall) { _pos(end, 50, printfCall.offsetTop + printfCall.offsetHeight + ALG_GAP);
if (start && end && printfCall && errEnd) {
_linkFlow(start, ".hook", printfCall, ".hook-in"); _linkFlow(start, ".hook", printfCall, ".hook-in");
_linkFlow(printfCall, ".hook-out", end, ".hook"); _linkFlow(printfCall, ".hook-out", end, ".hook");
_linkFlow(printfCall, ".proccall-error-hook", errEnd, ".hook");
} }
} }
}; };
@@ -156,9 +163,15 @@ TEMPLATES["sdl3-triangle"] = {
const end = $id("main-end"); const end = $id("main-end");
_pos(start, 50, 50); _pos(start, 50, 50);
/* Error exit — right of the init chain, collects all failure paths */
const errEnd = prog.newNode("wc-end");
_pos(errEnd, 420, start.offsetTop);
errEnd._selectByCode(1);
errEnd._enforceZeroEnd();
/* Variables column (x=700) */ /* Variables column (x=700) */
let varTop = start; let varTop = start;
function varBelow(block) { _placeBelow(varTop, block); varTop = block; } function varBelow(block) { _placeBelow(varTop, block, 20); varTop = block; }
const varRunning = _createBooleanVar(prog, "running", "true"); varBelow(varRunning); const varRunning = _createBooleanVar(prog, "running", "true"); varBelow(varRunning);
const varWindow = _createScalarVar(prog, "window"); varBelow(varWindow); const varWindow = _createScalarVar(prog, "window"); varBelow(varWindow);
@@ -338,7 +351,7 @@ TEMPLATES["sdl3-triangle"] = {
if (inputs[0]) inputs[0].value = "window"; if (inputs[0]) inputs[0].value = "window";
} }
const quitCall = _createProcCall(prog, "sdl3/SDL_Quit", ""); algBelow(quitCall); const quitCall = _createProcCall(prog, "sdl3/SDL_Quit", ""); algBelow(quitCall);
_pos(end, 50, prev.offsetTop + prev.offsetHeight + 16); _pos(end, 50, prev.offsetTop + prev.offsetHeight + ALG_GAP);
/* ── Wire up flow connections ── */ /* ── Wire up flow connections ── */
_linkFlow(start, ".hook", initCall, ".hook-in"); _linkFlow(start, ".hook", initCall, ".hook-in");
@@ -377,6 +390,11 @@ TEMPLATES["sdl3-triangle"] = {
if (renDestroy) _linkFlow(renDestroy, ".hook-out", winDestroy, ".hook-in"); if (renDestroy) _linkFlow(renDestroy, ".hook-out", winDestroy, ".hook-in");
if (winDestroy) _linkFlow(winDestroy, ".hook-out", quitCall, ".hook-in"); if (winDestroy) _linkFlow(winDestroy, ".hook-out", quitCall, ".hook-in");
if (quitCall) _linkFlow(quitCall, ".hook-out", end, ".hook"); if (quitCall) _linkFlow(quitCall, ".hook-out", end, ".hook");
/* ── Error paths — connect all proc calls with error hooks to the error END ── */
for (const block of [initCall, setMetaCall, winCall, renCall, renClear, renPresent]) {
if (block) _linkFlow(block, ".proccall-error-hook", errEnd, ".hook");
}
} }
}; };
+225
View File
@@ -473,6 +473,231 @@ const APP_TEST_SUITE = {
}); });
}); });
this._group("Error-path code generation", () => {
/* Helper: save start→end links, clear them, return a restore function */
const _disconnectStartEnd = () => {
const start = $id("main-start"), end = $id("main-end");
const saved = [...start.wcNextFlow];
for (const p of saved) deleteLink(p);
return () => _linkFlow(start, ".hook", end, ".hook");
};
this._test("simple error path generates if without else", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const errEnd = prog.newNode("wc-end");
errEnd._selectedCode = 1;
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", errEnd, ".hook");
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); errEnd.delete();
restore();
if (!code.includes("if")) throw Error("expected if");
if (code.includes("else")) throw Error("unexpected else: " + code);
});
this._test("error path emits exclusive success blocks after if", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const errEnd = prog.newNode("wc-end");
errEnd._selectedCode = 1;
const okAssign = prog.newNode("wc-assign");
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", errEnd, ".hook");
_linkFlow(call, ".hook-out", okAssign, ".hook-in");
_linkFlow(okAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); errEnd.delete(); okAssign.delete();
restore();
const ifIdx = code.indexOf("if");
const exitIdx = code.indexOf("exit(1)");
const returnIdx = code.indexOf("return 0");
if (ifIdx === -1) throw Error("missing if");
if (exitIdx === -1) throw Error("missing exit(1)");
if (returnIdx === -1) throw Error("missing return 0");
if (exitIdx > returnIdx) throw Error("exit after return (wrong order): " + code.substring(ifIdx, ifIdx + 100));
if (code.includes("else")) throw Error("unexpected else");
});
this._test("error path merging back omits shared blocks from branches", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const errAssign = prog.newNode("wc-assign");
const sharedAssign = prog.newNode("wc-assign");
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", errAssign, ".hook-in");
_linkFlow(call, ".hook-out", sharedAssign, ".hook-in");
_linkFlow(errAssign, "#hook-next", sharedAssign, ".hook-in");
_linkFlow(sharedAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); errAssign.delete(); sharedAssign.delete();
restore();
if (code.includes("else")) throw Error("unexpected else");
/* The shared assign should appear ONLY ONCE after the if block */
const first = code.indexOf("wc-assign");
const last = code.lastIndexOf("wc-assign");
if (first === -1) throw Error("assign code missing");
if (first !== last) throw Error("assign appears twice (shared block duplicated)");
});
this._test("error path through decision avoids else", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const errEnd = prog.newNode("wc-end");
errEnd._selectedCode = 1;
const dec = prog.newDecisionNode();
const okAssign = prog.newNode("wc-assign");
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", dec, "#hook-fore");
_linkFlow(dec, "#hook-out1", errEnd, ".hook");
_linkFlow(call, ".hook-out", okAssign, ".hook-in");
_linkFlow(okAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); errEnd.delete(); dec.delete(); okAssign.delete();
restore();
if (code.includes("else")) throw Error("unexpected else");
if (!code.includes("exit(1)")) throw Error("missing exit in error branch");
});
this._test("no error path emits plain call without if", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
// Don't add "visible" to errHook — no error path active
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete();
restore();
if (code.includes("if")) throw Error("unexpected if");
});
this._test("error path with value return style avoids else", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const errEnd = prog.newNode("wc-end");
errEnd._selectedCode = 1;
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "value";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", errEnd, ".hook");
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); errEnd.delete();
restore();
if (!code.includes("if")) throw Error("expected if");
if (code.includes("else")) throw Error("unexpected else: " + code);
});
this._test("sequential error-capable calls produce no else", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call1 = prog.newProcCallNode();
const call2 = prog.newProcCallNode();
const errEnd1 = prog.newNode("wc-end"); errEnd1._selectedCode = 1;
const errEnd2 = prog.newNode("wc-end"); errEnd2._selectedCode = 2;
call1._el.errHook.classList.add("visible");
call1._cachedReturnStyle = "error";
call2._el.errHook.classList.add("visible");
call2._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call1, ".hook-in");
_linkFlow(call1, ".proccall-error-hook", errEnd1, ".hook");
_linkFlow(call1, ".hook-out", call2, ".hook-in");
_linkFlow(call2, ".proccall-error-hook", errEnd2, ".hook");
_linkFlow(call2, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call1.delete(); call2.delete(); errEnd1.delete(); errEnd2.delete();
restore();
const ifCount = (code.match(/if\s*\(/g) || []).length;
if (ifCount !== 2) throw Error("expected 2 ifs, got " + ifCount);
if (code.includes("else")) throw Error("unexpected else");
});
this._test("error path with only shared blocks emits no duplicate", () => {
const restore = _disconnectStartEnd();
const start = $id("main-start"), mainEnd = $id("main-end");
const call = prog.newProcCallNode();
const shared = prog.newNode("wc-assign");
call._el.errHook.classList.add("visible");
call._cachedReturnStyle = "error";
_linkFlow(start, ".hook", call, ".hook-in");
_linkFlow(call, ".proccall-error-hook", shared, ".hook-in");
_linkFlow(call, ".hook-out", shared, ".hook-in");
_linkFlow(shared, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
call.delete(); shared.delete();
restore();
if (code.includes("else")) throw Error("unexpected else");
const first = code.indexOf("wc-assign");
const last = code.lastIndexOf("wc-assign");
if (first === -1) throw Error("assign code missing");
if (first !== last) throw Error("shared assign appears twice");
});
});
this._group("Error handling", () => { this._group("Error handling", () => {
this._test("showError displays overlay", () => { this._test("showError displays overlay", () => {
showError("test error"); showError("test error");