diff --git a/blocks-algo.js b/blocks-algo.js index a9542d0..0913f61 100644 --- a/blocks-algo.js +++ b/blocks-algo.js @@ -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; } } diff --git a/blocks-proc.js b/blocks-proc.js index f0dfe7b..5f5bbbe 100644 --- a/blocks-proc.js +++ b/blocks-proc.js @@ -167,30 +167,6 @@ function _specDataType(spec) { return "int"; } -/** - * Adapter: wraps CodeGen.branchDescendants with the browser's instanceof check. - * @param {HTMLElement[]} roots - * @returns {Set} - */ -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} stop - * @param {Set} 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() { diff --git a/codegen/index.js b/codegen/index.js index 194bb75..6eb4850 100644 --- a/codegen/index.js +++ b/codegen/index.js @@ -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"); diff --git a/tests/app/test-runner.js b/tests/app/test-runner.js index ca36c0f..ce2dd00 100644 --- a/tests/app/test-runner.js +++ b/tests/app/test-runner.js @@ -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", () => { diff --git a/tests/compile/Makefile b/tests/compile/Makefile index 4542b50..2bb7403 100644 --- a/tests/compile/Makefile +++ b/tests/compile/Makefile @@ -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) diff --git a/tests/compile/out/.gitignore b/tests/compile/out/.gitignore new file mode 100644 index 0000000..064a8d8 --- /dev/null +++ b/tests/compile/out/.gitignore @@ -0,0 +1 @@ +*.c diff --git a/tests/compile/out/.gitkeep b/tests/compile/out/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/compile/run.js b/tests/compile/run.js index 0e64270..ff66b56 100644 --- a/tests/compile/run.js +++ b/tests/compile/run.js @@ -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 ", "#include "], - declarations: " int result;", - }; + return { entry: start, includes: ["#include ", "#include "], 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 "], - declarations: "", - proto: ["int func(void);"], - }; + return { entry: start, includes: ["#include "], 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 "], - declarations: " int logged;\n int done;", - proto: ["int func(void);"], - }; + return { entry: start, includes: ["#include "], 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 "], - declarations: "", - proto: ["int init_a(void);", "int init_b(void);"], - }; + return { entry: start, includes: ["#include "], 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 "], - declarations: " int fd;", - proto: ["int open_file(const char *path, int flags);"], - }; + return { entry: start, includes: ["#include "], 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"); diff --git a/wcblock.js b/wcblock.js index a9b76b3..2077454 100644 --- a/wcblock.js +++ b/wcblock.js @@ -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). */ diff --git a/wcprogram.js b/wcprogram.js index 4a97a12..c9eed70 100644 --- a/wcprogram.js +++ b/wcprogram.js @@ -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);