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:
+83
-16
@@ -167,6 +167,52 @@ function _specDataType(spec) {
|
||||
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. */
|
||||
class WCProcCall extends WCBlock {
|
||||
connectedCallback() {
|
||||
@@ -379,6 +425,7 @@ class WCProcCall extends WCBlock {
|
||||
row.appendChild(input);
|
||||
this._el.fmtargsList.appendChild(row);
|
||||
this._setupHook(hook);
|
||||
this._wrapArgInput(input);
|
||||
}
|
||||
|
||||
_addFormatArg() {
|
||||
@@ -409,6 +456,7 @@ class WCProcCall extends WCBlock {
|
||||
row.appendChild(input);
|
||||
this._el.fmtargsList.appendChild(row);
|
||||
this._setupHook(hook);
|
||||
this._wrapArgInput(input);
|
||||
this._ensureFmtUnconnected();
|
||||
}
|
||||
|
||||
@@ -419,6 +467,18 @@ class WCProcCall extends WCBlock {
|
||||
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) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "proccall-arg-row";
|
||||
@@ -454,6 +514,7 @@ class WCProcCall extends WCBlock {
|
||||
|
||||
container.appendChild(row);
|
||||
this._setupHook(hook);
|
||||
this._wrapArgInput(input);
|
||||
}
|
||||
|
||||
_sourceName(block) {
|
||||
@@ -522,14 +583,7 @@ class WCProcCall extends WCBlock {
|
||||
if (returnStyle === "error") {
|
||||
const call = `${procName}(${inputs.join(", ")})`;
|
||||
if (hasErrPath) {
|
||||
const errCode = errPaths[0].toElement.generateCode(new Set());
|
||||
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 this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
|
||||
}
|
||||
return `\n${call};\n`;
|
||||
}
|
||||
@@ -537,14 +591,7 @@ class WCProcCall extends WCBlock {
|
||||
if (returnStyle === "value" && outputs.length === 1) {
|
||||
const outVar = outputs[0].replace(/^&/, "");
|
||||
if (hasErrPath) {
|
||||
const errCode = errPaths[0].toElement.generateCode(new Set());
|
||||
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 this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
|
||||
}
|
||||
return `\n${procName}(${inputs.join(", ")});\n`;
|
||||
}
|
||||
@@ -552,6 +599,26 @@ class WCProcCall extends WCBlock {
|
||||
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() {
|
||||
const args = {};
|
||||
for (const inp of this.querySelectorAll(".proccall-arg-input")) {
|
||||
|
||||
+23
-5
@@ -50,7 +50,8 @@ function _pos(el, left, top) {
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -133,11 +134,17 @@ TEMPLATES.console = {
|
||||
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(printfCall, ".hook-out", end, ".hook");
|
||||
_linkFlow(printfCall, ".proccall-error-hook", errEnd, ".hook");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -156,9 +163,15 @@ TEMPLATES["sdl3-triangle"] = {
|
||||
const end = $id("main-end");
|
||||
_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) */
|
||||
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 varWindow = _createScalarVar(prog, "window"); varBelow(varWindow);
|
||||
@@ -338,7 +351,7 @@ TEMPLATES["sdl3-triangle"] = {
|
||||
if (inputs[0]) inputs[0].value = "window";
|
||||
}
|
||||
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 ── */
|
||||
_linkFlow(start, ".hook", initCall, ".hook-in");
|
||||
@@ -377,6 +390,11 @@ TEMPLATES["sdl3-triangle"] = {
|
||||
if (renDestroy) _linkFlow(renDestroy, ".hook-out", winDestroy, ".hook-in");
|
||||
if (winDestroy) _linkFlow(winDestroy, ".hook-out", quitCall, ".hook-in");
|
||||
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");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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._test("showError displays overlay", () => {
|
||||
showError("test error");
|
||||
|
||||
Reference in New Issue
Block a user