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:
+8
-84
@@ -224,7 +224,7 @@ class WCOutput extends WCBlock {
|
||||
|
||||
// ── 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 {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
@@ -235,98 +235,22 @@ class WCDecision extends WCBlock {
|
||||
return resolveSourceName(block) || "flag";
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
_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 = [];
|
||||
generateFlowCode() {
|
||||
let yesTarget = null, noTarget = null;
|
||||
for (const path of this.wcNextFlow) {
|
||||
const hook = path.fromHook;
|
||||
if (!hook || hook.id === "hook-out1") {
|
||||
yesTargets.push(path.toElement);
|
||||
yesTarget = path.toElement;
|
||||
} 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 cond = source ? this._getBoolName(source) : "0";
|
||||
let out = "";
|
||||
let header = "";
|
||||
if (loopTargets && loopTargets.has(this)) {
|
||||
header = "\n" + this.id + ":\n";
|
||||
}
|
||||
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;
|
||||
if (yesTarget) out += ` if (${cond}) goto ${yesTarget.id};\n`;
|
||||
if (noTarget) out += ` goto ${noTarget.id};\n`;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-75
@@ -167,30 +167,6 @@ function _specDataType(spec) {
|
||||
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. */
|
||||
class WCProcCall extends WCBlock {
|
||||
connectedCallback() {
|
||||
@@ -501,30 +477,6 @@ class WCProcCall extends WCBlock {
|
||||
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() {
|
||||
const qname = this._selectedProc;
|
||||
if (!qname) return "";
|
||||
@@ -566,43 +518,87 @@ class WCProcCall extends WCBlock {
|
||||
}
|
||||
}
|
||||
|
||||
const errPaths = this._el.errHook && this._el.errHook.classList.contains("visible")
|
||||
? Array.from(this.wcNextFlow).filter(p => p.fromHook === this._el.errHook)
|
||||
: [];
|
||||
const hasErrPath = errPaths.length > 0;
|
||||
const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
|
||||
Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook);
|
||||
|
||||
// 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") {
|
||||
const call = `${procName}(${inputs.join(", ")})`;
|
||||
if (hasErrPath) {
|
||||
const result = this._generateCallWithError(`\nif (${call} != 0)`, errPaths);
|
||||
this._noWalkNext = true;
|
||||
return result;
|
||||
}
|
||||
return `\n${call};\n`;
|
||||
return ` ${procName}(${inputs.join(", ")});\n`;
|
||||
}
|
||||
|
||||
if (returnStyle === "value" && outputs.length === 1) {
|
||||
const outVar = outputs[0].replace(/^&/, "");
|
||||
if (hasErrPath) {
|
||||
const result = this._generateCallWithError(`\nif ((${outVar} = ${procName}(${inputs.join(", ")})) != 0)`, errPaths);
|
||||
this._noWalkNext = true;
|
||||
return result;
|
||||
}
|
||||
return `\n${procName}(${inputs.join(", ")});\n`;
|
||||
return ` ${procName}(${inputs.join(", ")});\n`;
|
||||
}
|
||||
const args = [...inputs, ...outputs].join(", ");
|
||||
return `\n${procName}(${args});\n`;
|
||||
return ` ${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);
|
||||
generateFlowCode() {
|
||||
const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
|
||||
Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook);
|
||||
if (!hasErrPath) return WCBlock.prototype.generateFlowCode.call(this);
|
||||
|
||||
return CodeGen.generateCallWithError(ifHeader, errTargets, okTargets, function (b) {
|
||||
return b instanceof WCDecision;
|
||||
}, this._loopTargets, this._genVisited);
|
||||
const qname = this._selectedProc;
|
||||
if (!qname) return "";
|
||||
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() {
|
||||
|
||||
+46
-150
@@ -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:
|
||||
// { id: string, generateOwnCode(): string, generateCode(visited, loopTargets): string,
|
||||
// wcNextFlow?: [{ toElement: block }], nextFlow?: [{ toElement: block }],
|
||||
// isDecision?: boolean }
|
||||
// Every block is a C label. Every flow edge is a `goto label;`.
|
||||
// There is zero nesting: no if/else chains, no branch-analysis, no
|
||||
// shared-block computation. Decisions become `if (cond) goto yes;
|
||||
// goto no;`. Error handling becomes `if (call != 0) goto err;
|
||||
// goto ok;`.
|
||||
//
|
||||
// Browser DOM blocks use wcNextFlow; test/mock blocks use nextFlow.
|
||||
// Decision blocks must expose isDecision = true.
|
||||
// Block interface:
|
||||
// { 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 () {
|
||||
"use strict";
|
||||
|
||||
var CodeGen = {};
|
||||
|
||||
// ── Back-edge detection ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Traverses the flow graph from a root and finds all back-edges
|
||||
* (edges to a block already on the current DFS path). Returns a Set
|
||||
* of blocks that are targets of at least one back-edge — these are
|
||||
* 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.
|
||||
* Emits a complete C program as a flat sequence of labelled blocks
|
||||
* connected by gotos. Each block is emitted once (DFS order); already-
|
||||
* visited blocks become `goto label;` stubs.
|
||||
*
|
||||
* @param {Array} roots starting blocks for this branch
|
||||
* @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
|
||||
* @param {Object} entry the first block (e.g. START)
|
||||
* @returns {string}
|
||||
*/
|
||||
CodeGen.emitBranch = function (roots, stop, visited, isDecision, loopTargets) {
|
||||
loopTargets = loopTargets || new Set();
|
||||
CodeGen.emitFlat = function (entry) {
|
||||
var visited = new Set();
|
||||
var parts = [];
|
||||
|
||||
function walk(el) {
|
||||
if (stop && stop.has(el)) return;
|
||||
if (visited.has(el)) {
|
||||
if (loopTargets.has(el)) {
|
||||
parts.push("\n goto " + (el.id || "block") + ";\n");
|
||||
}
|
||||
function emit(block) {
|
||||
if (visited.has(block)) {
|
||||
parts.push(" goto " + block.id + ";\n");
|
||||
return;
|
||||
}
|
||||
if (isDecision(el)) {
|
||||
parts.push(el.generateCode(visited, loopTargets));
|
||||
visited.add(block);
|
||||
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 {
|
||||
visited.add(el);
|
||||
if (loopTargets.has(el)) {
|
||||
parts.push("\n" + (el.id || "block") + ":");
|
||||
for (var i = 0; i < flow.length; i++) {
|
||||
var t = flow[i].toElement;
|
||||
if (t) fc += " goto " + t.id + ";\n";
|
||||
}
|
||||
el._genVisited = visited;
|
||||
parts.push(el.generateOwnCode());
|
||||
// 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;
|
||||
}
|
||||
if (fc) parts.push("\n" + fc);
|
||||
|
||||
// Recurse into successors (visited blocks are skipped at top)
|
||||
for (var i = 0; i < flow.length; i++) {
|
||||
var t = flow[i].toElement;
|
||||
if (t) emit(t);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < roots.length; i++) {
|
||||
if (!visited.has(roots[i])) walk(roots[i]);
|
||||
}
|
||||
emit(entry);
|
||||
return parts.join("");
|
||||
};
|
||||
|
||||
// ── Error-path code generation ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generates the error-path branch for a proc call: computes shared blocks
|
||||
* between error and success paths, emits error-exclusive code inside `if`,
|
||||
* then success-exclusive and shared code after it (no else).
|
||||
* Assembles a complete C source file.
|
||||
*/
|
||||
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) {
|
||||
var lines = ["// --- WiseCode\u00ae ---"];
|
||||
for (var i = 0; i < includes.length; i++) lines.push(includes[i]);
|
||||
lines.push("");
|
||||
lines.push("int main() {");
|
||||
if (declarations) lines.push(declarations);
|
||||
|
||||
var loopTargets = CodeGen.findBackEdges(entryBlock);
|
||||
var body = entryBlock.generateCode(new Set(), loopTargets);
|
||||
lines.push(body);
|
||||
lines.push(CodeGen.emitFlat(entryBlock));
|
||||
lines.push("}");
|
||||
lines.push("// --- END ---");
|
||||
return lines.join("\n");
|
||||
|
||||
@@ -98,28 +98,7 @@ const APP_TEST_SUITE = {
|
||||
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", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -Werror=implicit-function-declaration -Werror=incompatible-pointer-types -fsyntax-only
|
||||
OUTDIR ?= /tmp/wisecode-compile
|
||||
CFLAGS ?= -Wall -Wextra -fsyntax-only
|
||||
OUTDIR ?= out
|
||||
|
||||
SRC := $(wildcard $(OUTDIR)/*.c)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.c
|
||||
+39
-172
@@ -1,13 +1,9 @@
|
||||
#!/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,
|
||||
* then runs `make` to compile them with `gcc -Wall -Wextra -fsyntax-only`.
|
||||
*
|
||||
* 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.
|
||||
* Block data objects are minimal: generateOwnCode() + optional generateFlowCode().
|
||||
* The shared CodeGen.emitFlat walks the graph, emitting labels + gotos.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
@@ -15,156 +11,73 @@ const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
const CodeGen = require("../../codegen/index.js");
|
||||
|
||||
const OUTDIR = "/tmp/wisecode-compile";
|
||||
const OUTDIR = path.join(__dirname, "out");
|
||||
const MKFILE = path.join(__dirname, "Makefile");
|
||||
|
||||
// ── Block data helpers ────────────────────────────────────────────────
|
||||
// Plain objects matching the interface CodeGen expects:
|
||||
// { generateOwnCode(), generateCode(visited), nextFlow, isDecision }
|
||||
|
||||
function startBlock(id) {
|
||||
return {
|
||||
id: id,
|
||||
isDecision: false,
|
||||
nextFlow: [],
|
||||
generateOwnCode: function () {
|
||||
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;
|
||||
},
|
||||
generateOwnCode: function () { return ""; },
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
id: id,
|
||||
isDecision: false,
|
||||
nextFlow: [],
|
||||
generateOwnCode: function () {
|
||||
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;
|
||||
},
|
||||
generateOwnCode: function () { return code; },
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(targets) {
|
||||
return (targets || []).map(function (t) {
|
||||
return { toElement: t };
|
||||
});
|
||||
}
|
||||
|
||||
function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTargets, errTargets) {
|
||||
var call = procName + "(" + inputs.join(", ") + ")";
|
||||
var self = {
|
||||
id: id,
|
||||
isDecision: false,
|
||||
nextFlow: wrap(okTargets),
|
||||
nextFlow: (okTargets || []).concat(errTargets || []).map(function (t) { return { toElement: t }; }),
|
||||
errTargets: errTargets || [],
|
||||
generateOwnCode: function () {
|
||||
if (self.errTargets.length === 0) return "\n " + call + ";\n";
|
||||
var ifHeader;
|
||||
if (returnStyle === "value" && returnValueName) {
|
||||
ifHeader = " if ((" + returnValueName + " = " + call + ") != 0)";
|
||||
} else {
|
||||
ifHeader = " if (" + call + " != 0)";
|
||||
if (returnStyle === "error" && self.errTargets.length > 0) return "";
|
||||
if (returnStyle === "value" && returnValueName && self.errTargets.length > 0) {
|
||||
return " " + returnValueName + " = " + call + ";\n";
|
||||
}
|
||||
var okTargets = self.nextFlow.map(function (p) {
|
||||
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;
|
||||
return " " + call + ";\n";
|
||||
},
|
||||
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 "";
|
||||
generateFlowCode: function () {
|
||||
if (self.errTargets.length === 0) {
|
||||
return self.nextFlow.map(function (p) { return " goto " + p.toElement.id + ";\n"; }).join("");
|
||||
}
|
||||
visited.add(this);
|
||||
var own = this.generateOwnCode();
|
||||
if (loopTargets && loopTargets.has(this)) own = "\n" + this.id + ":\n" + own;
|
||||
if (self.errTargets.length > 0) return own;
|
||||
for (var i = 0; i < this.nextFlow.length; i++) {
|
||||
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);
|
||||
}
|
||||
var errT = self.errTargets[0];
|
||||
var okT = null;
|
||||
for (var i = 0; i < self.nextFlow.length; i++) {
|
||||
var t = self.nextFlow[i].toElement;
|
||||
if (self.errTargets.indexOf(t) === -1) { okT = t; break; }
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function assignBlock(id, target, value) {
|
||||
var code = "\n " + target + " = " + value + ";\n";
|
||||
return {
|
||||
id: id,
|
||||
isDecision: false,
|
||||
nextFlow: [],
|
||||
generateOwnCode: function () {
|
||||
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;
|
||||
},
|
||||
generateOwnCode: function () { return " " + target + " = " + value + ";\n"; },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,21 +91,9 @@ function buildConsoleTemplate() {
|
||||
var start = startBlock("start");
|
||||
var end = endBlock("end", 0);
|
||||
var errEnd = endBlock("errEnd", 1);
|
||||
var printf = procCallBlock(
|
||||
"printf",
|
||||
"printf",
|
||||
['"Hello, World!\\n"'],
|
||||
"value",
|
||||
"result",
|
||||
[end],
|
||||
[errEnd]
|
||||
);
|
||||
var printf = procCallBlock("printf", "printf", ['"Hello, World!\\n"'], "value", "result", [end], [errEnd]);
|
||||
link(start, printf);
|
||||
return {
|
||||
entry: start,
|
||||
includes: ["#include <stdio.h>", "#include <stdlib.h>"],
|
||||
declarations: " int result;",
|
||||
};
|
||||
return { entry: start, includes: ["#include <stdio.h>", "#include <stdlib.h>"], declarations: " int result;" };
|
||||
}
|
||||
|
||||
function buildErrorSimpleTemplate() {
|
||||
@@ -201,12 +102,7 @@ function buildErrorSimpleTemplate() {
|
||||
var errEnd = endBlock("errEnd", 1);
|
||||
var call = procCallBlock("init", "func", [], "error", null, [end], [errEnd]);
|
||||
link(start, call);
|
||||
return {
|
||||
entry: start,
|
||||
includes: ["#include <stdlib.h>"],
|
||||
declarations: "",
|
||||
proto: ["int func(void);"],
|
||||
};
|
||||
return { entry: start, includes: ["#include <stdlib.h>"], declarations: "", proto: ["int func(void);"] };
|
||||
}
|
||||
|
||||
function buildErrorMergeTemplate() {
|
||||
@@ -218,12 +114,7 @@ function buildErrorMergeTemplate() {
|
||||
link(logged, done);
|
||||
link(done, end);
|
||||
link(start, call);
|
||||
return {
|
||||
entry: start,
|
||||
includes: ["#include <stdlib.h>"],
|
||||
declarations: " int logged;\n int done;",
|
||||
proto: ["int func(void);"],
|
||||
};
|
||||
return { entry: start, includes: ["#include <stdlib.h>"], declarations: " int logged;\n int done;", proto: ["int func(void);"] };
|
||||
}
|
||||
|
||||
function errorSequentialTemplate() {
|
||||
@@ -235,34 +126,16 @@ function errorSequentialTemplate() {
|
||||
var call2 = procCallBlock("b", "init_b", [], "error", null, [end], [errEnd2]);
|
||||
link(start, call1);
|
||||
link(call1, call2);
|
||||
return {
|
||||
entry: start,
|
||||
includes: ["#include <stdlib.h>"],
|
||||
declarations: "",
|
||||
proto: ["int init_a(void);", "int init_b(void);"],
|
||||
};
|
||||
return { entry: start, includes: ["#include <stdlib.h>"], declarations: "", proto: ["int init_a(void);", "int init_b(void);"] };
|
||||
}
|
||||
|
||||
function buildErrorValueTemplate() {
|
||||
var start = startBlock("start");
|
||||
var end = endBlock("end", 0);
|
||||
var errEnd = endBlock("errEnd", 1);
|
||||
var call = procCallBlock(
|
||||
"open",
|
||||
"open_file",
|
||||
['"data.txt"', "0"],
|
||||
"value",
|
||||
"fd",
|
||||
[end],
|
||||
[errEnd]
|
||||
);
|
||||
var call = procCallBlock("open", "open_file", ['"data.txt"', "0"], "value", "fd", [end], [errEnd]);
|
||||
link(start, call);
|
||||
return {
|
||||
entry: start,
|
||||
includes: ["#include <stdlib.h>"],
|
||||
declarations: " int fd;",
|
||||
proto: ["int open_file(const char *path, int flags);"],
|
||||
};
|
||||
return { entry: start, includes: ["#include <stdlib.h>"], declarations: " int fd;", proto: ["int open_file(const char *path, int flags);"] };
|
||||
}
|
||||
|
||||
// ── Source generation ──────────────────────────────────────────────────
|
||||
@@ -276,8 +149,7 @@ function generateSource(opts) {
|
||||
parts.push("");
|
||||
parts.push("int main() {");
|
||||
if (opts.declarations) parts.push(opts.declarations);
|
||||
var loopTargets = CodeGen.findBackEdges(opts.entry);
|
||||
parts.push(opts.entry.generateCode(new Set(), loopTargets));
|
||||
parts.push(CodeGen.emitFlat(opts.entry));
|
||||
parts.push("}");
|
||||
parts.push("// --- END ---");
|
||||
return parts.join("\n");
|
||||
@@ -294,9 +166,7 @@ function writeSource(label, source) {
|
||||
|
||||
console.log("\nGenerating C compilation tests...\n");
|
||||
|
||||
try {
|
||||
execSync("rm -f " + OUTDIR + "/*.c", { stdio: "pipe" });
|
||||
} catch (_) {}
|
||||
try { execSync("rm -f " + OUTDIR + "/*.c", { stdio: "pipe" }); } catch (_) {}
|
||||
|
||||
writeSource("test_console", generateSource(buildConsoleTemplate()));
|
||||
writeSource("test_error_simple", generateSource(buildErrorSimpleTemplate()));
|
||||
@@ -307,15 +177,12 @@ writeSource("test_error_value", generateSource(buildErrorValueTemplate()));
|
||||
console.log("\nRunning make...\n");
|
||||
|
||||
try {
|
||||
execSync('make -C "' + OUTDIR + '" -f "' + MKFILE + '" OUTDIR="' + OUTDIR + '" all 2>&1', {
|
||||
stdio: "inherit",
|
||||
});
|
||||
execSync('make -C "' + OUTDIR + '" -f "' + MKFILE + '" OUTDIR="' + OUTDIR + '" all 2>&1', { stdio: "inherit" });
|
||||
} catch (e) {
|
||||
console.error("\n\u2717 Compilation failed.\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try SDL3
|
||||
try {
|
||||
execSync("pkg-config --exists sdl3", { stdio: "pipe" });
|
||||
console.log("\nSDL3 headers found, generating SDL3 test...\n");
|
||||
|
||||
+12
-37
@@ -247,51 +247,26 @@ class WCBlock extends HTMLElement {
|
||||
generateOwnCode() { return ""; }
|
||||
|
||||
/**
|
||||
* Generates C code for this block and its successors, respecting the visited set
|
||||
* to avoid infinite loops. Only blocks in loopTargets get C labels; only
|
||||
* back-edges to loop targets emit `goto`.
|
||||
* @param {Set} [visited]
|
||||
* @param {Set} [loopTargets] blocks that are targets of back-edges
|
||||
* C flow-control: `goto` statements for every successor in wcNextFlow.
|
||||
* Override for decisions (if-goto) and error-handling proc calls.
|
||||
* @returns {string}
|
||||
*/
|
||||
generateCode(visited = new Set(), loopTargets) {
|
||||
if (!this._isFlowBlock) return this.generateOwnCode();
|
||||
if (visited.has(this)) {
|
||||
if (loopTargets && loopTargets.has(this)) {
|
||||
return "\n goto " + this.id + ";\n";
|
||||
}
|
||||
return "";
|
||||
generateFlowCode() {
|
||||
let code = "";
|
||||
for (const path of this.wcNextFlow) {
|
||||
code += " goto " + path.toElement.id + ";\n";
|
||||
}
|
||||
visited.add(this);
|
||||
let out = "";
|
||||
if (loopTargets && loopTargets.has(this)) {
|
||||
out += "\n" + this.id + ":\n";
|
||||
}
|
||||
out += this.generateOwnCode() + this.generateNextCode(visited, loopTargets);
|
||||
return out;
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks wcNextFlow paths and accumulates code from downstream blocks.
|
||||
* When a target is already visited (loop back-edge) AND is a loop target,
|
||||
* emits `goto label;`. Otherwise silently skips the edge.
|
||||
* @param {Set} visited
|
||||
* @param {Set} [loopTargets]
|
||||
* Generates C code for variable declarations (non-flow blocks only).
|
||||
* For flow blocks, use CodeGen.emitFlat instead.
|
||||
* @returns {string}
|
||||
*/
|
||||
generateNextCode(visited, loopTargets) {
|
||||
let code = "";
|
||||
for (const path of this.wcNextFlow) {
|
||||
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;
|
||||
generateCode(visited) {
|
||||
if (!this._isFlowBlock) return this.generateOwnCode();
|
||||
return ""; // flow blocks are handled by CodeGen.emitFlat
|
||||
}
|
||||
|
||||
/** @returns {number} The CSS `--grid-size` value, cached after first read (default 25). */
|
||||
|
||||
+2
-4
@@ -226,10 +226,8 @@ class WCProgram extends HTMLElement {
|
||||
: "";
|
||||
|
||||
const firstChild = this.firstElementChild;
|
||||
const loopTargets = firstChild ? CodeGen.findBackEdges(firstChild) : new Set();
|
||||
for (const b of this.querySelectorAll("[data-block]")) b._loopTargets = loopTargets;
|
||||
const body = firstChild && typeof firstChild.generateCode === "function"
|
||||
? firstChild.generateCode(new Set(), loopTargets)
|
||||
const body = firstChild && typeof CodeGen.emitFlat === "function"
|
||||
? CodeGen.emitFlat(firstChild)
|
||||
: "\n// (no flowchart body)\n";
|
||||
const proccalls = this.querySelectorAll("wc-proccall");
|
||||
const includes = this._generateIncludes(proccalls, hasStrings);
|
||||
|
||||
Reference in New Issue
Block a user