refactor: flat DFA code generation — every block is a label, every edge is a goto

Replace the nested if-else branch-analysis architecture with a flat DFA
model using CodeGen.emitFlat — a single DFS walk emitting each block as
a C label followed by its code and gotos to all successors.

Removed:
- _branchDescendants / emitBranch / generateCallWithError
- _generateCallWithError, _noWalkNext, _genVisited
- generateNextCode, findBackEdges
- WCDecision._branchDescendants / _emitBranch / overridden generateCode

Added:
- CodeGen.emitFlat(entry) — flat DFS emit with labels + gotos
- WCBlock.generateFlowCode() — base emits gotos from wcNextFlow
- WCDecision.generateFlowCode() — if-goto / goto for decisions
- WCProcCall.generateFlowCode() — if (call != 0) goto err / goto ok

Output is flat — zero nested if-else for branch/error handling.
Tests use local out/ directory.
This commit is contained in:
2026-06-23 03:21:22 +02:00
parent 9b2c8da5dc
commit 35959784de
10 changed files with 181 additions and 545 deletions
+8 -84
View File
@@ -224,7 +224,7 @@ class WCOutput extends WCBlock {
// ── WCDecision — conditional branch ──────────────────────────────────────── // ── WCDecision — conditional branch ────────────────────────────────────────
/** Conditional branch block; splits flow into yes/no branches and emits if/else C code. */ /** Conditional branch block; emits if-goto / goto for flat DFA code generation. */
class WCDecision extends WCBlock { class WCDecision extends WCBlock {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -235,98 +235,22 @@ class WCDecision extends WCBlock {
return resolveSourceName(block) || "flag"; return resolveSourceName(block) || "flag";
} }
_branchDescendants(roots) { generateFlowCode() {
const d = new Set(), q = [...roots]; let yesTarget = null, noTarget = null;
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;
}
_emitBranch(roots, stop, visited, loopTargets) {
let c = "";
const walk = el => {
if (stop && stop.has(el)) return;
if (visited.has(el)) {
if (loopTargets && loopTargets.has(el)) {
c += "\n goto " + el.id + ";\n";
}
return;
}
if (el instanceof WCDecision) {
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();
if (!el._noWalkNext) {
for (const p of el.wcNextFlow) walk(p.toElement);
}
el._noWalkNext = false;
}
};
for (const el of roots) {
if (!visited.has(el)) walk(el);
}
return c;
}
generateCode(visited = new Set(), loopTargets) {
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) {
return "\n goto " + this.id + ";\n";
}
return "";
}
visited.add(this);
const yesTargets = [], noTargets = [];
for (const path of this.wcNextFlow) { for (const path of this.wcNextFlow) {
const hook = path.fromHook; const hook = path.fromHook;
if (!hook || hook.id === "hook-out1") { if (!hook || hook.id === "hook-out1") {
yesTargets.push(path.toElement); yesTarget = path.toElement;
} else if (hook.id === "hook-out0") { } else if (hook.id === "hook-out0") {
noTargets.push(path.toElement); noTarget = path.toElement;
} }
} }
const yesAll = this._branchDescendants(yesTargets);
const noAll = this._branchDescendants(noTargets);
const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this));
const yesCode = this._emitBranch(yesTargets, shared, visited, loopTargets);
const noCode = this._emitBranch(noTargets, shared, visited, loopTargets);
const sharedCode = this._emitBranch([...shared], new Set(), visited, loopTargets);
for (const el of yesAll) visited.add(el);
for (const el of noAll) visited.add(el);
const source = findDataSource(this._el.boolIn); const source = findDataSource(this._el.boolIn);
const cond = source ? this._getBoolName(source) : "0"; const cond = source ? this._getBoolName(source) : "0";
let out = ""; let out = "";
let header = ""; if (yesTarget) out += ` if (${cond}) goto ${yesTarget.id};\n`;
if (loopTargets && loopTargets.has(this)) { if (noTarget) out += ` goto ${noTarget.id};\n`;
header = "\n" + this.id + ":\n"; return out;
}
if (yesCode || noCode) {
if (yesCode) out += `if (${cond}) {\n${yesCode}}`;
if (noCode) {
out += yesCode
? ` else {\n${noCode}}`
: `if (!(${cond})) {\n${noCode}}`;
}
}
out += sharedCode;
return out ? header + out + "\n" : header;
} }
} }
+71 -75
View File
@@ -167,30 +167,6 @@ function _specDataType(spec) {
return "int"; return "int";
} }
/**
* Adapter: wraps CodeGen.branchDescendants with the browser's instanceof check.
* @param {HTMLElement[]} roots
* @returns {Set<HTMLElement>}
*/
function _branchDescendants(roots) {
return CodeGen.branchDescendants(roots, function (b) {
return b instanceof WCDecision;
});
}
/**
* Adapter: wraps CodeGen.emitBranch with the browser's instanceof check.
* @param {HTMLElement[]} roots
* @param {Set<HTMLElement>} stop
* @param {Set<HTMLElement>} visited
* @returns {string}
*/
function _emitBranch(roots, stop, visited) {
return CodeGen.emitBranch(roots, stop, visited, function (b) {
return b instanceof WCDecision;
});
}
/** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */ /** Procedure call block with dynamic argument fields, format-string support, and error-handling hooks. Lives in the Algorithm tab. */
class WCProcCall extends WCBlock { class WCProcCall extends WCBlock {
connectedCallback() { connectedCallback() {
@@ -501,30 +477,6 @@ class WCProcCall extends WCBlock {
return block.dataType === "string" ? name + ".data" : name; return block.dataType === "string" ? name + ".data" : name;
} }
generateCode(visited = new Set(), loopTargets) {
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) {
return "\n goto " + this.id + ";\n";
}
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();
this._genVisited = null;
if (loopTargets && loopTargets.has(this)) {
own = "\n" + this.id + ":\n" + own;
}
if (this._el.errHook && this._el.errHook.classList.contains("visible") &&
Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook)) {
return own;
}
return own + this.generateNextCode(visited, loopTargets);
}
generateOwnCode() { generateOwnCode() {
const qname = this._selectedProc; const qname = this._selectedProc;
if (!qname) return ""; if (!qname) return "";
@@ -566,43 +518,87 @@ class WCProcCall extends WCBlock {
} }
} }
const errPaths = this._el.errHook && this._el.errHook.classList.contains("visible") const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
? Array.from(this.wcNextFlow).filter(p => p.fromHook === this._el.errHook) Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook);
: [];
const hasErrPath = errPaths.length > 0; // For error-capable calls, the call is emitted inside generateFlowCode
// (as part of the if-condition) to avoid double execution.
if (returnStyle === "error" && hasErrPath) return "";
if (returnStyle === "value" && outputs.length === 1 && hasErrPath) {
const outVar = outputs[0].replace(/^&/, "");
return ` ${outVar} = ${procName}(${inputs.join(", ")});\n`;
}
if (returnStyle === "error") { if (returnStyle === "error") {
const call = `${procName}(${inputs.join(", ")})`; return ` ${procName}(${inputs.join(", ")});\n`;
if (hasErrPath) {
const result = this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
this._noWalkNext = true;
return result;
}
return `\n${call};\n`;
} }
if (returnStyle === "value" && outputs.length === 1) { if (returnStyle === "value" && outputs.length === 1) {
const outVar = outputs[0].replace(/^&/, ""); return ` ${procName}(${inputs.join(", ")});\n`;
if (hasErrPath) {
const result = this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
this._noWalkNext = true;
return result;
}
return `\n${procName}(${inputs.join(", ")});\n`;
} }
const args = [...inputs, ...outputs].join(", "); const args = [...inputs, ...outputs].join(", ");
return `\n${procName}(${args});\n`; return ` ${procName}(${args});\n`;
} }
_generateCallWithError(ifHeader, errPaths) { generateFlowCode() {
const errTargets = errPaths.map(p => p.toElement); const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
const okTargets = Array.from(this.wcNextFlow) Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook);
.filter(p => p.fromHook !== this._el.errHook) if (!hasErrPath) return WCBlock.prototype.generateFlowCode.call(this);
.map(p => p.toElement);
return CodeGen.generateCallWithError(ifHeader, errTargets, okTargets, function (b) { const qname = this._selectedProc;
return b instanceof WCDecision; if (!qname) return "";
}, this._loopTargets, this._genVisited); const parts = qname.split("/");
const procName = parts[1] || qname;
let returnStyle = this._cachedReturnStyle || "pointer";
const inputs = [];
const outputs = [];
for (const row of this._el.inputs.querySelectorAll(".proccall-arg-row")) {
const hook = row.querySelector(".hook");
const inp = row.querySelector(".proccall-arg-input");
const source = hook ? findDataSource(hook) : null;
if (source) {
inputs.push(this._sourceName(source));
} else {
inputs.push(inp.value || inp.placeholder);
}
}
for (const row of this._el.outputs.querySelectorAll(".proccall-arg-row")) {
const inp = row.querySelector(".proccall-arg-input");
outputs.push("&" + (inp.value || inp.placeholder));
}
if (this._fmtProc) {
for (const row of this._el.fmtargsList.querySelectorAll(".proccall-arg-row")) {
const hook = row.querySelector(".hook");
const inp = row.querySelector(".proccall-arg-input");
const source = hook ? findDataSource(hook) : null;
if (source) {
inputs.push(this._sourceName(source));
} else {
inputs.push(inp.value || inp.placeholder);
}
}
}
const args = inputs.join(", ");
const errTarget = Array.from(this.wcNextFlow)
.filter(p => p.fromHook === this._el.errHook)
.map(p => p.toElement)[0];
const okTarget = Array.from(this.wcNextFlow)
.filter(p => p.fromHook !== this._el.errHook)
.map(p => p.toElement)[0];
let code = "";
if (returnStyle === "error") {
code += ` if (${procName}(${args}) != 0) goto ${errTarget.id};\n`;
} else if (returnStyle === "value" && outputs.length === 1) {
const outVar = outputs[0].replace(/^&/, "");
code += ` if (${outVar} != 0) goto ${errTarget.id};\n`;
}
if (okTarget) code += ` goto ${okTarget.id};\n`;
return code;
} }
saveData() { saveData() {
+46 -150
View File
@@ -1,185 +1,81 @@
// ── Pure C code generation — no DOM dependencies ───────────────────────── // ── Pure C code generation — zero-DOM, flat DFA model ─────────────────
// //
// These functions operate on generic block objects with the interface: // Every block is a C label. Every flow edge is a `goto label;`.
// { id: string, generateOwnCode(): string, generateCode(visited, loopTargets): string, // There is zero nesting: no if/else chains, no branch-analysis, no
// wcNextFlow?: [{ toElement: block }], nextFlow?: [{ toElement: block }], // shared-block computation. Decisions become `if (cond) goto yes;
// isDecision?: boolean } // goto no;`. Error handling becomes `if (call != 0) goto err;
// goto ok;`.
// //
// Browser DOM blocks use wcNextFlow; test/mock blocks use nextFlow. // Block interface:
// Decision blocks must expose isDecision = true. // { id: string,
// generateOwnCode(): string, // C code for this one block
// generateFlowCode?(): string, // flow-control (gotos). If absent,
// // CodeGen walks wcNextFlow instead.
// wcNextFlow?: [{toElement: block}],
// nextFlow?: [{toElement: block}] }
(function () { (function () {
"use strict"; "use strict";
var CodeGen = {}; var CodeGen = {};
// ── Back-edge detection ──────────────────────────────────────────────
/** /**
* Traverses the flow graph from a root and finds all back-edges * Emits a complete C program as a flat sequence of labelled blocks
* (edges to a block already on the current DFS path). Returns a Set * connected by gotos. Each block is emitted once (DFS order); already-
* of blocks that are targets of at least one back-edge — these are * visited blocks become `goto label;` stubs.
* the only blocks that need C labels and can be goto targets.
* @param {Object} root
* @param {function(Object): Array} [getNext] returns [{toElement:..}]
* @param {function(Object): boolean} [isDecision]
* @returns {Set}
*/
CodeGen.findBackEdges = function (root, getNext, isDecision) {
isDecision = isDecision || function (b) { return b.isDecision; };
getNext = getNext || function (b) { return b.wcNextFlow || b.nextFlow || []; };
var visited = new Set();
var onPath = new Set();
var loopTargets = new Set();
function dfs(block) {
if (visited.has(block)) return;
visited.add(block);
onPath.add(block);
var flow = getNext(block);
for (var i = 0; i < flow.length; i++) {
var target = flow[i].toElement;
if (!target) continue;
if (onPath.has(target)) {
loopTargets.add(target);
} else if (!visited.has(target)) {
dfs(target);
}
}
onPath["delete"](block);
}
dfs(root);
return loopTargets;
};
// ── Branch analysis ──────────────────────────────────────────────────
/**
* Collects all blocks reachable from the given roots via nextFlow,
* excluding the interior of decision blocks.
* @param {Array} roots
* @param {function(Object): boolean} [isDecision]
* @returns {Set}
*/
CodeGen.branchDescendants = function (roots, isDecision) {
isDecision = isDecision || function (b) { return b.isDecision; };
var d = new Set(), q = roots.slice();
for (var i = 0; i < q.length; i++) {
var el = q[i];
if (d.has(el)) continue;
d.add(el);
if (isDecision(el)) continue;
var flow = el.wcNextFlow || el.nextFlow || [];
for (var j = 0; j < flow.length; j++) {
var target = flow[j].toElement;
if (target && !d.has(target)) q.push(target);
}
}
return d;
};
// ── Branch code emission ─────────────────────────────────────────────
/**
* Emits C code for a branch by walking from root blocks. Only blocks
* in loopTargets get C labels / gotos. The stop set prevents shared
* blocks from being emitted in individual branches.
* *
* @param {Array} roots starting blocks for this branch * @param {Object} entry the first block (e.g. START)
* @param {Set} stop blocks to exclude (shared between branches)
* @param {Set} visited blocks already emitted (modified in-place)
* @param {function(Object): boolean} isDecision
* @param {Set} loopTargets back-edge targets from findBackEdges
* @returns {string} * @returns {string}
*/ */
CodeGen.emitBranch = function (roots, stop, visited, isDecision, loopTargets) { CodeGen.emitFlat = function (entry) {
loopTargets = loopTargets || new Set(); var visited = new Set();
var parts = []; var parts = [];
function walk(el) { function emit(block) {
if (stop && stop.has(el)) return; if (visited.has(block)) {
if (visited.has(el)) { parts.push(" goto " + block.id + ";\n");
if (loopTargets.has(el)) {
parts.push("\n goto " + (el.id || "block") + ";\n");
}
return; return;
} }
if (isDecision(el)) { visited.add(block);
parts.push(el.generateCode(visited, loopTargets)); parts.push("\n" + block.id + ":");
var own = block.generateOwnCode();
if (own) parts.push(own);
// Flow control: custom generateFlowCode, or auto from wcNextFlow
var flow = block.wcNextFlow || block.nextFlow || [];
var fc = "";
if (typeof block.generateFlowCode === "function") {
fc = block.generateFlowCode();
} else { } else {
visited.add(el); for (var i = 0; i < flow.length; i++) {
if (loopTargets.has(el)) { var t = flow[i].toElement;
parts.push("\n" + (el.id || "block") + ":"); if (t) fc += " goto " + t.id + ";\n";
} }
el._genVisited = visited; }
parts.push(el.generateOwnCode()); if (fc) parts.push("\n" + fc);
// If the block's generateOwnCode already walked the success path
// (e.g. error-handling proc call), don't re-walk its nextFlow. // Recurse into successors (visited blocks are skipped at top)
if (!el._noWalkNext) { for (var i = 0; i < flow.length; i++) {
var flow = el.wcNextFlow || el.nextFlow || []; var t = flow[i].toElement;
for (var j = 0; j < flow.length; j++) { if (t) emit(t);
walk(flow[j].toElement);
}
}
el._noWalkNext = false;
} }
} }
for (var i = 0; i < roots.length; i++) { emit(entry);
if (!visited.has(roots[i])) walk(roots[i]);
}
return parts.join(""); return parts.join("");
}; };
// ── Error-path code generation ───────────────────────────────────────
/** /**
* Generates the error-path branch for a proc call: computes shared blocks * Assembles a complete C source file.
* between error and success paths, emits error-exclusive code inside `if`,
* then success-exclusive and shared code after it (no else).
*/ */
CodeGen.generateCallWithError = function (
ifHeader, errTargets, okTargets, isDecision, loopTargets, mainVisited
) {
loopTargets = loopTargets || new Set();
var errAll = CodeGen.branchDescendants(errTargets, isDecision);
var okAll = CodeGen.branchDescendants(okTargets, isDecision);
var shared = new Set();
errAll.forEach(function (b) {
if (okAll.has(b)) shared.add(b);
});
// 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(
Array.from(shared), new Set(), visited, isDecision, loopTargets
);
var out = ifHeader + " {\n" + errCode + "}";
if (okCode) out += " else {\n" + okCode + "}";
out += "\n";
if (sharedCode) out += sharedCode;
return out;
};
// ── Full program assembly ────────────────────────────────────────────
CodeGen.generateCFile = function (entryBlock, includes, declarations) { CodeGen.generateCFile = function (entryBlock, includes, declarations) {
var lines = ["// --- WiseCode\u00ae ---"]; var lines = ["// --- WiseCode\u00ae ---"];
for (var i = 0; i < includes.length; i++) lines.push(includes[i]); for (var i = 0; i < includes.length; i++) lines.push(includes[i]);
lines.push(""); lines.push("");
lines.push("int main() {"); lines.push("int main() {");
if (declarations) lines.push(declarations); if (declarations) lines.push(declarations);
lines.push(CodeGen.emitFlat(entryBlock));
var loopTargets = CodeGen.findBackEdges(entryBlock);
var body = entryBlock.generateCode(new Set(), loopTargets);
lines.push(body);
lines.push("}"); lines.push("}");
lines.push("// --- END ---"); lines.push("// --- END ---");
return lines.join("\n"); return lines.join("\n");
-21
View File
@@ -98,28 +98,7 @@ const APP_TEST_SUITE = {
if (!code.includes("return 0")) throw Error("missing return 0"); if (!code.includes("return 0")) throw Error("missing return 0");
}); });
this._test("branchDescendants traverses past nested decision", () => {
const dA = prog.newDecisionNode();
const dB = prog.newDecisionNode();
const aX = prog.newNode("wc-assign");
dA.style.left = "200px"; dA.style.top = "50px";
dB.style.left = "400px"; dB.style.top = "50px";
aX.style.left = "600px"; aX.style.top = "50px";
const start = $id("main-start");
const end = $id("main-end");
const savedPaths = [...start.wcNextFlow];
for (const p of savedPaths) deleteLink(p);
_linkFlow(start, ".hook", dA, "#hook-fore");
_linkFlow(dA, "#hook-out1", dB, "#hook-fore");
_linkFlow(dB, "#hook-out1", aX, "#hook-fore");
const desc = dA._branchDescendants([dB]);
dA.delete(); dB.delete(); aX.delete();
if (!desc.has(aX)) throw Error("block after nested decision not reached");
});
}); });
this._group("Variable operations", () => { this._group("Variable operations", () => {
+2 -2
View File
@@ -1,6 +1,6 @@
CC ?= gcc CC ?= gcc
CFLAGS ?= -Wall -Wextra -Werror=implicit-function-declaration -Werror=incompatible-pointer-types -fsyntax-only CFLAGS ?= -Wall -Wextra -fsyntax-only
OUTDIR ?= /tmp/wisecode-compile OUTDIR ?= out
SRC := $(wildcard $(OUTDIR)/*.c) SRC := $(wildcard $(OUTDIR)/*.c)
+1
View File
@@ -0,0 +1 @@
*.c
View File
+39 -172
View File
@@ -1,13 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* C compilation test runner. * C compilation test runner — flat DFA code generation.
* *
* Generates C source files matching each template's expected output, * Block data objects are minimal: generateOwnCode() + optional generateFlowCode().
* then runs `make` to compile them with `gcc -Wall -Wextra -fsyntax-only`. * The shared CodeGen.emitFlat walks the graph, emitting labels + gotos.
*
* Block data objects are created manually (matching the browser template
* structure) and passed to the shared CodeGen module for graph walking
* and code emission — no duplication of branch/emit logic.
*/ */
const fs = require("fs"); const fs = require("fs");
@@ -15,156 +11,73 @@ const path = require("path");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const CodeGen = require("../../codegen/index.js"); const CodeGen = require("../../codegen/index.js");
const OUTDIR = "/tmp/wisecode-compile"; const OUTDIR = path.join(__dirname, "out");
const MKFILE = path.join(__dirname, "Makefile"); const MKFILE = path.join(__dirname, "Makefile");
// ── Block data helpers ──────────────────────────────────────────────── // ── Block data helpers ────────────────────────────────────────────────
// Plain objects matching the interface CodeGen expects:
// { generateOwnCode(), generateCode(visited), nextFlow, isDecision }
function startBlock(id) { function startBlock(id) {
return { return {
id: id, id: id,
isDecision: false, isDecision: false,
nextFlow: [], nextFlow: [],
generateOwnCode: function () { generateOwnCode: function () { return ""; },
return "";
},
generateCode: function (visited, loopTargets) {
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) return "\n goto " + this.id + ";\n";
return "";
}
visited.add(this);
var c = "";
if (loopTargets && loopTargets.has(this)) c += "\n" + this.id + ":\n";
c += this.generateOwnCode();
for (var i = 0; i < this.nextFlow.length; i++) {
var t = this.nextFlow[i].toElement;
if (visited.has(t)) {
if (loopTargets && loopTargets.has(t)) c += "\n goto " + t.id + ";\n";
} else {
c += t.generateCode(visited, loopTargets);
}
}
return c;
},
}; };
} }
function endBlock(id, exitCode) { function endBlock(id, exitCode) {
var code = exitCode === 0 ? "\n return 0;\n" : "\n exit(" + exitCode + ");\n"; var code = exitCode === 0 ? " return 0;\n" : " exit(" + exitCode + ");\n";
return { return {
id: id, id: id,
isDecision: false, isDecision: false,
nextFlow: [], nextFlow: [],
generateOwnCode: function () { generateOwnCode: function () { return code; },
return code;
},
generateCode: function (visited, loopTargets) {
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) return "\n goto " + this.id + ";\n";
return "";
}
visited.add(this);
var c = "";
if (loopTargets && loopTargets.has(this)) c += "\n" + this.id + ":\n";
c += this.generateOwnCode();
return c;
},
}; };
} }
function wrap(targets) {
return (targets || []).map(function (t) {
return { toElement: t };
});
}
function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTargets, errTargets) { function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTargets, errTargets) {
var call = procName + "(" + inputs.join(", ") + ")"; var call = procName + "(" + inputs.join(", ") + ")";
var self = { var self = {
id: id, id: id,
isDecision: false, isDecision: false,
nextFlow: wrap(okTargets), nextFlow: (okTargets || []).concat(errTargets || []).map(function (t) { return { toElement: t }; }),
errTargets: errTargets || [], errTargets: errTargets || [],
generateOwnCode: function () { generateOwnCode: function () {
if (self.errTargets.length === 0) return "\n " + call + ";\n"; if (returnStyle === "error" && self.errTargets.length > 0) return "";
var ifHeader; if (returnStyle === "value" && returnValueName && self.errTargets.length > 0) {
if (returnStyle === "value" && returnValueName) { return " " + returnValueName + " = " + call + ";\n";
ifHeader = " if ((" + returnValueName + " = " + call + ") != 0)";
} else {
ifHeader = " if (" + call + " != 0)";
} }
var okTargets = self.nextFlow.map(function (p) { return " " + call + ";\n";
return p.toElement;
});
var result = CodeGen.generateCallWithError(
"\n" + ifHeader,
self.errTargets,
okTargets,
function (b) {
return b.isDecision;
},
self._loopTargets,
self._genVisited
);
self._noWalkNext = true;
return result;
}, },
generateCode: function (visited, loopTargets) { generateFlowCode: function () {
self._loopTargets = loopTargets; if (self.errTargets.length === 0) {
self._genVisited = visited; return self.nextFlow.map(function (p) { return " goto " + p.toElement.id + ";\n"; }).join("");
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) return "\n goto " + this.id + ";\n";
return "";
} }
visited.add(this); var errT = self.errTargets[0];
var own = this.generateOwnCode(); var okT = null;
if (loopTargets && loopTargets.has(this)) own = "\n" + this.id + ":\n" + own; for (var i = 0; i < self.nextFlow.length; i++) {
if (self.errTargets.length > 0) return own; var t = self.nextFlow[i].toElement;
for (var i = 0; i < this.nextFlow.length; i++) { if (self.errTargets.indexOf(t) === -1) { okT = t; break; }
var t = this.nextFlow[i].toElement;
if (visited.has(t)) {
if (loopTargets && loopTargets.has(t)) own += "\n goto " + t.id + ";\n";
} else {
own += t.generateCode(visited, loopTargets);
}
} }
return own; var out = "";
if (returnStyle === "error") {
out += " if (" + call + " != 0) goto " + errT.id + ";\n";
} else if (returnStyle === "value" && returnValueName) {
out += " if (" + returnValueName + " != 0) goto " + errT.id + ";\n";
}
if (okT) out += " goto " + okT.id + ";\n";
return out;
}, },
}; };
return self; return self;
} }
function assignBlock(id, target, value) { function assignBlock(id, target, value) {
var code = "\n " + target + " = " + value + ";\n";
return { return {
id: id, id: id,
isDecision: false, isDecision: false,
nextFlow: [], nextFlow: [],
generateOwnCode: function () { generateOwnCode: function () { return " " + target + " = " + value + ";\n"; },
return code;
},
generateCode: function (visited, loopTargets) {
if (visited.has(this)) {
if (loopTargets && loopTargets.has(this)) return "\n goto " + this.id + ";\n";
return "";
}
visited.add(this);
var c = "";
if (loopTargets && loopTargets.has(this)) c += "\n" + this.id + ":\n";
c += this.generateOwnCode();
for (var i = 0; i < this.nextFlow.length; i++) {
var t = this.nextFlow[i].toElement;
if (visited.has(t)) {
if (loopTargets && loopTargets.has(t)) c += "\n goto " + t.id + ";\n";
} else {
c += t.generateCode(visited, loopTargets);
}
}
return c;
},
}; };
} }
@@ -178,21 +91,9 @@ function buildConsoleTemplate() {
var start = startBlock("start"); var start = startBlock("start");
var end = endBlock("end", 0); var end = endBlock("end", 0);
var errEnd = endBlock("errEnd", 1); var errEnd = endBlock("errEnd", 1);
var printf = procCallBlock( var printf = procCallBlock("printf", "printf", ['"Hello, World!\\n"'], "value", "result", [end], [errEnd]);
"printf",
"printf",
['"Hello, World!\\n"'],
"value",
"result",
[end],
[errEnd]
);
link(start, printf); link(start, printf);
return { return { entry: start, includes: ["#include <stdio.h>", "#include <stdlib.h>"], declarations: " int result;" };
entry: start,
includes: ["#include <stdio.h>", "#include <stdlib.h>"],
declarations: " int result;",
};
} }
function buildErrorSimpleTemplate() { function buildErrorSimpleTemplate() {
@@ -201,12 +102,7 @@ function buildErrorSimpleTemplate() {
var errEnd = endBlock("errEnd", 1); var errEnd = endBlock("errEnd", 1);
var call = procCallBlock("init", "func", [], "error", null, [end], [errEnd]); var call = procCallBlock("init", "func", [], "error", null, [end], [errEnd]);
link(start, call); link(start, call);
return { return { entry: start, includes: ["#include <stdlib.h>"], declarations: "", proto: ["int func(void);"] };
entry: start,
includes: ["#include <stdlib.h>"],
declarations: "",
proto: ["int func(void);"],
};
} }
function buildErrorMergeTemplate() { function buildErrorMergeTemplate() {
@@ -218,12 +114,7 @@ function buildErrorMergeTemplate() {
link(logged, done); link(logged, done);
link(done, end); link(done, end);
link(start, call); link(start, call);
return { return { entry: start, includes: ["#include <stdlib.h>"], declarations: " int logged;\n int done;", proto: ["int func(void);"] };
entry: start,
includes: ["#include <stdlib.h>"],
declarations: " int logged;\n int done;",
proto: ["int func(void);"],
};
} }
function errorSequentialTemplate() { function errorSequentialTemplate() {
@@ -235,34 +126,16 @@ function errorSequentialTemplate() {
var call2 = procCallBlock("b", "init_b", [], "error", null, [end], [errEnd2]); var call2 = procCallBlock("b", "init_b", [], "error", null, [end], [errEnd2]);
link(start, call1); link(start, call1);
link(call1, call2); link(call1, call2);
return { return { entry: start, includes: ["#include <stdlib.h>"], declarations: "", proto: ["int init_a(void);", "int init_b(void);"] };
entry: start,
includes: ["#include <stdlib.h>"],
declarations: "",
proto: ["int init_a(void);", "int init_b(void);"],
};
} }
function buildErrorValueTemplate() { function buildErrorValueTemplate() {
var start = startBlock("start"); var start = startBlock("start");
var end = endBlock("end", 0); var end = endBlock("end", 0);
var errEnd = endBlock("errEnd", 1); var errEnd = endBlock("errEnd", 1);
var call = procCallBlock( var call = procCallBlock("open", "open_file", ['"data.txt"', "0"], "value", "fd", [end], [errEnd]);
"open",
"open_file",
['"data.txt"', "0"],
"value",
"fd",
[end],
[errEnd]
);
link(start, call); link(start, call);
return { return { entry: start, includes: ["#include <stdlib.h>"], declarations: " int fd;", proto: ["int open_file(const char *path, int flags);"] };
entry: start,
includes: ["#include <stdlib.h>"],
declarations: " int fd;",
proto: ["int open_file(const char *path, int flags);"],
};
} }
// ── Source generation ────────────────────────────────────────────────── // ── Source generation ──────────────────────────────────────────────────
@@ -276,8 +149,7 @@ function generateSource(opts) {
parts.push(""); parts.push("");
parts.push("int main() {"); parts.push("int main() {");
if (opts.declarations) parts.push(opts.declarations); if (opts.declarations) parts.push(opts.declarations);
var loopTargets = CodeGen.findBackEdges(opts.entry); parts.push(CodeGen.emitFlat(opts.entry));
parts.push(opts.entry.generateCode(new Set(), loopTargets));
parts.push("}"); parts.push("}");
parts.push("// --- END ---"); parts.push("// --- END ---");
return parts.join("\n"); return parts.join("\n");
@@ -294,9 +166,7 @@ function writeSource(label, source) {
console.log("\nGenerating C compilation tests...\n"); console.log("\nGenerating C compilation tests...\n");
try { try { execSync("rm -f " + OUTDIR + "/*.c", { stdio: "pipe" }); } catch (_) {}
execSync("rm -f " + OUTDIR + "/*.c", { stdio: "pipe" });
} catch (_) {}
writeSource("test_console", generateSource(buildConsoleTemplate())); writeSource("test_console", generateSource(buildConsoleTemplate()));
writeSource("test_error_simple", generateSource(buildErrorSimpleTemplate())); writeSource("test_error_simple", generateSource(buildErrorSimpleTemplate()));
@@ -307,15 +177,12 @@ writeSource("test_error_value", generateSource(buildErrorValueTemplate()));
console.log("\nRunning make...\n"); console.log("\nRunning make...\n");
try { try {
execSync('make -C "' + OUTDIR + '" -f "' + MKFILE + '" OUTDIR="' + OUTDIR + '" all 2>&1', { execSync('make -C "' + OUTDIR + '" -f "' + MKFILE + '" OUTDIR="' + OUTDIR + '" all 2>&1', { stdio: "inherit" });
stdio: "inherit",
});
} catch (e) { } catch (e) {
console.error("\n\u2717 Compilation failed.\n"); console.error("\n\u2717 Compilation failed.\n");
process.exit(1); process.exit(1);
} }
// Try SDL3
try { try {
execSync("pkg-config --exists sdl3", { stdio: "pipe" }); execSync("pkg-config --exists sdl3", { stdio: "pipe" });
console.log("\nSDL3 headers found, generating SDL3 test...\n"); console.log("\nSDL3 headers found, generating SDL3 test...\n");
+12 -37
View File
@@ -247,51 +247,26 @@ class WCBlock extends HTMLElement {
generateOwnCode() { return ""; } generateOwnCode() { return ""; }
/** /**
* Generates C code for this block and its successors, respecting the visited set * C flow-control: `goto` statements for every successor in wcNextFlow.
* to avoid infinite loops. Only blocks in loopTargets get C labels; only * Override for decisions (if-goto) and error-handling proc calls.
* back-edges to loop targets emit `goto`.
* @param {Set} [visited]
* @param {Set} [loopTargets] blocks that are targets of back-edges
* @returns {string} * @returns {string}
*/ */
generateCode(visited = new Set(), loopTargets) { generateFlowCode() {
if (!this._isFlowBlock) return this.generateOwnCode(); let code = "";
if (visited.has(this)) { for (const path of this.wcNextFlow) {
if (loopTargets && loopTargets.has(this)) { code += " goto " + path.toElement.id + ";\n";
return "\n goto " + this.id + ";\n";
}
return "";
} }
visited.add(this); return code;
let out = "";
if (loopTargets && loopTargets.has(this)) {
out += "\n" + this.id + ":\n";
}
out += this.generateOwnCode() + this.generateNextCode(visited, loopTargets);
return out;
} }
/** /**
* Walks wcNextFlow paths and accumulates code from downstream blocks. * Generates C code for variable declarations (non-flow blocks only).
* When a target is already visited (loop back-edge) AND is a loop target, * For flow blocks, use CodeGen.emitFlat instead.
* emits `goto label;`. Otherwise silently skips the edge.
* @param {Set} visited
* @param {Set} [loopTargets]
* @returns {string} * @returns {string}
*/ */
generateNextCode(visited, loopTargets) { generateCode(visited) {
let code = ""; if (!this._isFlowBlock) return this.generateOwnCode();
for (const path of this.wcNextFlow) { return ""; // flow blocks are handled by CodeGen.emitFlat
const target = path.toElement;
if (visited.has(target)) {
if (loopTargets && loopTargets.has(target)) {
code += "\n goto " + target.id + ";\n";
}
} else {
code += target.generateCode(visited, loopTargets);
}
}
return code;
} }
/** @returns {number} The CSS `--grid-size` value, cached after first read (default 25). */ /** @returns {number} The CSS `--grid-size` value, cached after first read (default 25). */
+2 -4
View File
@@ -226,10 +226,8 @@ class WCProgram extends HTMLElement {
: ""; : "";
const firstChild = this.firstElementChild; const firstChild = this.firstElementChild;
const loopTargets = firstChild ? CodeGen.findBackEdges(firstChild) : new Set(); const body = firstChild && typeof CodeGen.emitFlat === "function"
for (const b of this.querySelectorAll("[data-block]")) b._loopTargets = loopTargets; ? CodeGen.emitFlat(firstChild)
const body = firstChild && typeof firstChild.generateCode === "function"
? firstChild.generateCode(new Set(), loopTargets)
: "\n// (no flowchart body)\n"; : "\n// (no flowchart body)\n";
const proccalls = this.querySelectorAll("wc-proccall"); const proccalls = this.querySelectorAll("wc-proccall");
const includes = this._generateIncludes(proccalls, hasStrings); const includes = this._generateIncludes(proccalls, hasStrings);