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)
This commit is contained in:
2026-06-23 01:16:14 +02:00
parent f1eed2d4e0
commit 439cc6e7ba
5 changed files with 69 additions and 44 deletions
+12 -10
View File
@@ -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;
}
}
+2 -2
View File
@@ -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;
+17 -17
View File
@@ -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
);
+26 -11
View File
@@ -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;
},
+12 -4
View File
@@ -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;
}