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:
+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");
|
||||
|
||||
Reference in New Issue
Block a user