diff --git a/blocks-proc.js b/blocks-proc.js index a5894a3..c7d8844 100644 --- a/blocks-proc.js +++ b/blocks-proc.js @@ -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} + */ +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} stop blocks to exclude (shared between branches) + * @param {Set} 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")) { diff --git a/templates.js b/templates.js index 4eecfd9..18b1a45 100644 --- a/templates.js +++ b/templates.js @@ -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"); + } } }; diff --git a/tests/app/test-runner.js b/tests/app/test-runner.js index bf74a7d..f981b3b 100644 --- a/tests/app/test-runner.js +++ b/tests/app/test-runner.js @@ -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");