From 439cc6e7bafd6083af129e682652d0a25191fc00 Mon Sep 17 00:00:00 2001 From: Adrien Jaguenet Date: Tue, 23 Jun 2026 01:16:14 +0200 Subject: [PATCH] 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) --- blocks-algo.js | 22 ++++++++++++---------- blocks-proc.js | 4 ++-- codegen/index.js | 34 +++++++++++++++++----------------- tests/compile/run.js | 37 ++++++++++++++++++++++++++----------- wcblock.js | 16 ++++++++++++---- 5 files changed, 69 insertions(+), 44 deletions(-) diff --git a/blocks-algo.js b/blocks-algo.js index b639b0a..8f876f9 100644 --- a/blocks-algo.js +++ b/blocks-algo.js @@ -249,23 +249,25 @@ class WCDecision extends WCBlock { } _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); + 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) walk(el); + 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; @@ -299,7 +301,7 @@ class WCDecision extends WCBlock { } } out += sharedCode; - return out ? `\n${out}\n` : ""; + return out ? header + out + "\n" : header; } } diff --git a/blocks-proc.js b/blocks-proc.js index d5e6420..9c98adc 100644 --- a/blocks-proc.js +++ b/blocks-proc.js @@ -502,9 +502,9 @@ class WCProcCall extends WCBlock { } generateCode(visited = new Set()) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - const own = this.generateOwnCode(); + const own = "\n" + this.id + ":\n" + this.generateOwnCode(); if (this._el.errHook && this._el.errHook.classList.contains("visible") && Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook)) { return own; diff --git a/codegen/index.js b/codegen/index.js index cb37646..8ecbf5a 100644 --- a/codegen/index.js +++ b/codegen/index.js @@ -46,8 +46,9 @@ /** * Emits C code for a branch by walking from root blocks, stopping at - * blocks in the stop set and at decision boundaries. Each visited block - * is added to the visited set to prevent double-emission. + * blocks in the stop set and at decision boundaries. When a loop + * back-edge encounters an already-visited block, a `goto label;` is + * emitted instead of silently dropping the edge. * @param {Array} roots * @param {Set} stop blocks to exclude (shared between branches) * @param {Set} visited blocks already emitted @@ -60,24 +61,20 @@ function (b) { return b.isDecision; }; - var v = new Set(visited); var parts = []; function walk(el) { - if (v.has(el) || (stop && stop.has(el))) return; - v.add(el); - if (isDecision(el)) { - parts.push(el.generateCode(v)); - } else { - parts.push(el.generateOwnCode()); - var flow = el.wcNextFlow || el.nextFlow || []; - for (var j = 0; j < flow.length; j++) { - walk(flow[j].toElement); - } + if (stop && stop.has(el)) return; + if (visited.has(el)) { + parts.push("\n goto " + (el.id || "block") + ";\n"); + return; } + parts.push(el.generateCode(visited)); } - for (var i = 0; i < roots.length; i++) walk(roots[i]); + for (var i = 0; i < roots.length; i++) { + if (!visited.has(roots[i])) walk(roots[i]); + } return parts.join(""); }; @@ -104,12 +101,15 @@ if (okAll.has(b)) shared.add(b); }); - var errCode = CodeGen.emitBranch(errTargets, shared, new Set(), isDecision); - var okCode = CodeGen.emitBranch(okTargets, shared, new Set(), isDecision); + // Thread a shared visited set through all three emitBranch calls + // so shared blocks are not re-emitted with duplicate labels. + var visited = new Set(); + var errCode = CodeGen.emitBranch(errTargets, shared, visited, isDecision); + var okCode = CodeGen.emitBranch(okTargets, shared, visited, isDecision); var sharedCode = CodeGen.emitBranch( Array.from(shared), new Set(), - new Set(), + visited, isDecision ); diff --git a/tests/compile/run.js b/tests/compile/run.js index 34bc8c6..2d6563e 100644 --- a/tests/compile/run.js +++ b/tests/compile/run.js @@ -31,11 +31,16 @@ function startBlock(id) { return ""; }, generateCode: function (visited) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - var c = this.generateOwnCode(); + var c = "\n" + this.id + ":\n" + this.generateOwnCode(); for (var i = 0; i < this.nextFlow.length; i++) { - c += this.nextFlow[i].toElement.generateCode(visited); + var t = this.nextFlow[i].toElement; + if (visited.has(t)) { + c += "\n goto " + t.id + ";\n"; + } else { + c += t.generateCode(visited); + } } return c; }, @@ -52,9 +57,9 @@ function endBlock(id, exitCode) { return code; }, generateCode: function (visited) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - return this.generateOwnCode(); + return "\n" + this.id + ":\n" + this.generateOwnCode(); }, }; } @@ -93,12 +98,17 @@ function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTar ); }, generateCode: function (visited) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - var own = this.generateOwnCode(); + var own = "\n" + this.id + ":\n" + this.generateOwnCode(); if (self.errTargets.length > 0) return own; for (var i = 0; i < this.nextFlow.length; i++) { - own += this.nextFlow[i].toElement.generateCode(visited); + var t = this.nextFlow[i].toElement; + if (visited.has(t)) { + own += "\n goto " + t.id + ";\n"; + } else { + own += t.generateCode(visited); + } } return own; }, @@ -116,11 +126,16 @@ function assignBlock(id, target, value) { return code; }, generateCode: function (visited) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - var c = this.generateOwnCode(); + var c = "\n" + this.id + ":\n" + this.generateOwnCode(); for (var i = 0; i < this.nextFlow.length; i++) { - c += this.nextFlow[i].toElement.generateCode(visited); + var t = this.nextFlow[i].toElement; + if (visited.has(t)) { + c += "\n goto " + t.id + ";\n"; + } else { + c += t.generateCode(visited); + } } return c; }, diff --git a/wcblock.js b/wcblock.js index 49d38ea..2400126 100644 --- a/wcblock.js +++ b/wcblock.js @@ -247,25 +247,33 @@ class WCBlock extends HTMLElement { /** * Generates C code for this block and its successors, respecting the visited set - * to avoid infinite loops. + * to avoid infinite loops. Each block emits a C label (its id) so that loop + * back-edges can target it with `goto label;`. * @param {Set} [visited] * @returns {string} */ generateCode(visited = new Set()) { - if (visited.has(this)) return ""; + if (visited.has(this)) return "\n goto " + this.id + ";\n"; visited.add(this); - return this.generateOwnCode() + this.generateNextCode(visited); + return "\n" + this.id + ":\n" + this.generateOwnCode() + this.generateNextCode(visited); } /** * Walks wcNextFlow paths and accumulates code from downstream blocks. + * When a target is already visited (loop back-edge), emits `goto label;` + * instead of silently dropping the path. * @param {Set} visited * @returns {string} */ generateNextCode(visited) { let code = ""; for (const path of this.wcNextFlow) { - code += path.toElement.generateCode(visited); + const target = path.toElement; + if (visited.has(target)) { + code += "\n goto " + target.id + ";\n"; + } else { + code += target.generateCode(visited); + } } return code; }