fix: seed _generateCallWithError's local visited set from main traversal

- _emitBranch now sets _genVisited on blocks before calling generateOwnCode
  so that proc calls inside loop bodies can seed their error-handling
  visited set from the main traversal, preventing loop-decision re-walk
- Add _noWalkNext flag on proc calls when error handling is active, so
  _emitBranch skips redundant nextFlow walking (the success path was
  already covered by the error-handling branch emission)
- Known issue: shared blocks that are direct roots of emitBranch (from
  the shared set) may still be re-walked when they were already emitted
  by a downstream proc call's error-handling success path
This commit is contained in:
2026-06-23 03:08:20 +02:00
parent cefee37cd8
commit 400870d888
4 changed files with 38 additions and 16 deletions
+7 -2
View File
@@ -259,15 +259,20 @@ class WCDecision extends WCBlock {
return;
}
if (el instanceof WCDecision) {
// Decisions manage their own visited tracking inside generateCode.
c += el.generateCode(visited, loopTargets);
} else {
visited.add(el);
if (loopTargets && loopTargets.has(el)) {
c += "\n" + el.id + ":";
}
// Let proc calls know about the visited set so their error-path
// generation uses it instead of a fresh local set.
el._genVisited = visited;
c += el.generateOwnCode();
for (const p of el.wcNextFlow) walk(p.toElement);
if (!el._noWalkNext) {
for (const p of el.wcNextFlow) walk(p.toElement);
}
el._noWalkNext = false;
}
};
for (const el of roots) {
+12 -6
View File
@@ -509,10 +509,12 @@ class WCProcCall extends WCBlock {
return "";
}
visited.add(this);
// Store the main-traversal visited set so _generateCallWithError can
// use it instead of a fresh local set — prevents re-walking blocks
// (like a loop-decision) that are already covered by the main traversal.
this._genVisited = visited;
let own = this.generateOwnCode();
// The PROC CALL itself might be targeted by a back-edge for the success path.
// That's handled by generateNextCode on the predecessor block.
// If this block is a loop target, emit the label before ownCode.
this._genVisited = null;
if (loopTargets && loopTargets.has(this)) {
own = "\n" + this.id + ":\n" + own;
}
@@ -572,7 +574,9 @@ class WCProcCall extends WCBlock {
if (returnStyle === "error") {
const call = `${procName}(${inputs.join(", ")})`;
if (hasErrPath) {
return this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
const result = this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
this._noWalkNext = true;
return result;
}
return `\n${call};\n`;
}
@@ -580,7 +584,9 @@ class WCProcCall extends WCBlock {
if (returnStyle === "value" && outputs.length === 1) {
const outVar = outputs[0].replace(/^&/, "");
if (hasErrPath) {
return this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
const result = this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
this._noWalkNext = true;
return result;
}
return `\n${procName}(${inputs.join(", ")});\n`;
}
@@ -596,7 +602,7 @@ class WCProcCall extends WCBlock {
return CodeGen.generateCallWithError(ifHeader, errTargets, okTargets, function (b) {
return b instanceof WCDecision;
}, this._loopTargets);
}, this._loopTargets, this._genVisited);
}
saveData() {
+13 -6
View File
@@ -108,18 +108,23 @@
return;
}
if (isDecision(el)) {
// Decisions manage their own visited tracking inside generateCode.
parts.push(el.generateCode(visited, loopTargets));
} else {
visited.add(el);
if (loopTargets.has(el)) {
parts.push("\n" + (el.id || "block") + ":");
}
el._genVisited = visited;
parts.push(el.generateOwnCode());
var flow = el.wcNextFlow || el.nextFlow || [];
for (var j = 0; j < flow.length; j++) {
walk(flow[j].toElement);
// If the block's generateOwnCode already walked the success path
// (e.g. error-handling proc call), don't re-walk its nextFlow.
if (!el._noWalkNext) {
var flow = el.wcNextFlow || el.nextFlow || [];
for (var j = 0; j < flow.length; j++) {
walk(flow[j].toElement);
}
}
el._noWalkNext = false;
}
}
@@ -137,7 +142,7 @@
* then success-exclusive and shared code after it (no else).
*/
CodeGen.generateCallWithError = function (
ifHeader, errTargets, okTargets, isDecision, loopTargets
ifHeader, errTargets, okTargets, isDecision, loopTargets, mainVisited
) {
loopTargets = loopTargets || new Set();
var errAll = CodeGen.branchDescendants(errTargets, isDecision);
@@ -147,7 +152,9 @@
if (okAll.has(b)) shared.add(b);
});
var visited = new Set();
// Create a local visited seeded from the main set so blocks walked by
// the error/success branches don't pollute the main traversal's set.
var visited = new Set(mainVisited || []);
var errCode = CodeGen.emitBranch(errTargets, shared, visited, isDecision, loopTargets);
var okCode = CodeGen.emitBranch(okTargets, shared, visited, isDecision, loopTargets);
var sharedCode = CodeGen.emitBranch(
+6 -2
View File
@@ -99,18 +99,22 @@ function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTar
var okTargets = self.nextFlow.map(function (p) {
return p.toElement;
});
return CodeGen.generateCallWithError(
var result = CodeGen.generateCallWithError(
"\n" + ifHeader,
self.errTargets,
okTargets,
function (b) {
return b.isDecision;
},
self._loopTargets
self._loopTargets,
self._genVisited
);
self._noWalkNext = true;
return result;
},
generateCode: function (visited, loopTargets) {
self._loopTargets = loopTargets;
self._genVisited = visited;
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) return "\n goto " + this.id + ";\n";
return "";