feat: switch from C to LLVM IR code generation

- Replace all C code generation with LLVM IR (flat DFA model with
  bb_ prefix labels to avoid SSA value name collisions)
- Remove c.min.js, add llvm.min.js highlight.js language definition
- Update shell.nix: gcc -> clang, remove sdl3/mesa deps
- Download LLVM Language Reference to docs/llvm/LangRef.html
- Rename #c-code to #ir-code in HTML, JS, and CSS
- Replace _generateIncludes with _generateLLVMDecls for extern
  function declarations using LLVM_DECL_MAP
- Add string pool (CodeGen.allocString, stringGlobal, llvmStringArg)
  for LLVM IR global string constants with getelementptr references
This commit is contained in:
2026-06-23 03:39:22 +02:00
parent 35959784de
commit d53f92482d
12 changed files with 289 additions and 296 deletions
+1
View File
@@ -2,3 +2,4 @@
vendor/highlight/*
vendor/mathjax/*
!vendor/download.sh
docs/llvm/
+15 -8
View File
@@ -177,13 +177,13 @@ class WCEnd extends WCBlock {
generateOwnCode() {
const code = this._selectedCode;
if (code === 0) {
const frees = this._genStringFrees();
if (frees) return `\n${frees} return 0;\n`;
return "\n return 0;\n";
return " ret i32 0\n";
}
return `\n exit(${code});\n`;
return " call void @exit(i32 " + code + ")\n unreachable\n";
}
generateFlowCode() { return ""; }
saveData() { return { exitCode: this._selectedCode }; }
restoreData(d) {
this._selectByCode(d.exitCode !== undefined ? d.exitCode : 0);
@@ -235,6 +235,15 @@ class WCDecision extends WCBlock {
return resolveSourceName(block) || "flag";
}
generateOwnCode() {
const source = findDataSource(this._el.boolIn);
if (source) {
const name = resolveSourceName(source) || "flag";
return " %_c = load i1, i1* %" + name + "\n";
}
return " %_c = alloca i1\n store i1 0, i1* %_c\n %_c = load i1, i1* %_c\n";
}
generateFlowCode() {
let yesTarget = null, noTarget = null;
for (const path of this.wcNextFlow) {
@@ -245,11 +254,9 @@ class WCDecision extends WCBlock {
noTarget = path.toElement;
}
}
const source = findDataSource(this._el.boolIn);
const cond = source ? this._getBoolName(source) : "0";
let out = "";
if (yesTarget) out += ` if (${cond}) goto ${yesTarget.id};\n`;
if (noTarget) out += ` goto ${noTarget.id};\n`;
if (yesTarget) out += " br i1 %_c, label %" + yesTarget.id + ", label %" + (noTarget ? noTarget.id : "") + "\n";
else if (noTarget) out += " br label %" + noTarget.id + "\n";
return out;
}
}
+64 -98
View File
@@ -477,127 +477,93 @@ class WCProcCall extends WCBlock {
return block.dataType === "string" ? name + ".data" : name;
}
generateOwnCode() {
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;
_buildLLVMArgs() {
var inputs = [];
var outputs = [];
for (var ri = 0; ri < this._el.inputs.querySelectorAll(".proccall-arg-row").length; ri++) {
var row = this._el.inputs.querySelectorAll(".proccall-arg-row")[ri];
var hook = row.querySelector(".hook");
var inp = row.querySelector(".proccall-arg-input");
var source = hook ? findDataSource(hook) : null;
if (source) {
inputs.push(this._sourceName(source));
inputs.push("%" + this._sourceName(source));
} else {
let val = inp.value || inp.placeholder;
if (inp.dataset.paramType === "string") val = `"${escapeString(val)}"`;
var val = inp.value || inp.placeholder;
if (inp.dataset.paramType === "string") {
var idx = CodeGen.allocString(val);
val = CodeGen.llvmStringArg(val, idx);
} else if (inp.dataset.paramType === "int" || inp.dataset.paramType === "i32") {
val = "i32 " + val;
} else if (inp.dataset.paramType === "float" || inp.dataset.paramType === "double") {
val = "double " + val;
} else {
val = "i32 " + val; // default integer
}
inputs.push(val);
}
}
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 {
let val = inp.value || inp.placeholder;
if (hook.dataset.type === "string") val = `"${escapeString(val)}"`;
inputs.push(val);
}
}
for (var rj = 0; rj < this._el.outputs.querySelectorAll(".proccall-arg-row").length; rj++) {
var row2 = this._el.outputs.querySelectorAll(".proccall-arg-row")[rj];
var inp2 = row2.querySelector(".proccall-arg-input");
outputs.push(inp2.value || inp2.placeholder);
}
return { inputs: inputs, outputs: outputs };
}
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`;
}
generateOwnCode() {
var qname = this._selectedProc;
if (!qname) return "";
var sp = qname.split("/");
var procName = sp[1] || qname;
var returnStyle = this._cachedReturnStyle || "pointer";
var io = this._buildLLVMArgs();
var callArgs = io.inputs.join(", ");
var hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
Array.from(this.wcNextFlow).some(function (p) { return p.fromHook === this._el.errHook; }.bind(this));
var rn = "%_r_" + this.id, vn = "%_v_" + this.id;
if (returnStyle === "error") {
return ` ${procName}(${inputs.join(", ")});\n`;
if (hasErrPath) return " " + rn + " = call i32 @" + procName + "(" + callArgs + ")\n";
return " call void @" + procName + "(" + callArgs + ")\n";
}
if (returnStyle === "value" && outputs.length === 1) {
return ` ${procName}(${inputs.join(", ")});\n`;
if (returnStyle === "value" && io.outputs.length === 1) {
if (hasErrPath) return " " + vn + " = call i32 @" + procName + "(" + callArgs + ")\n";
return " call void @" + procName + "(" + callArgs + ")\n";
}
const args = [...inputs, ...outputs].join(", ");
return ` ${procName}(${args});\n`;
return " call void @" + procName + "(" + callArgs + ")\n";
}
generateFlowCode() {
const hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
Array.from(this.wcNextFlow).some(p => p.fromHook === this._el.errHook);
var hasErrPath = this._el.errHook && this._el.errHook.classList.contains("visible") &&
Array.from(this.wcNextFlow).some(function (p) { return p.fromHook === this._el.errHook; }.bind(this));
if (!hasErrPath) return WCBlock.prototype.generateFlowCode.call(this);
const qname = this._selectedProc;
var qname = this._selectedProc;
if (!qname) return "";
const parts = qname.split("/");
const procName = parts[1] || qname;
var sp = qname.split("/");
var procName = sp[1] || qname;
var returnStyle = this._cachedReturnStyle || "pointer";
var io = this._buildLLVMArgs();
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);
}
}
var errTarget = null, okTarget = null;
var flows = Array.from(this.wcNextFlow);
for (var fi = 0; fi < flows.length; fi++) {
var p = flows[fi];
if (p.fromHook === this._el.errHook) errTarget = p.toElement;
else okTarget = p.toElement;
}
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 = "";
var rn = "%_r_" + this.id, vn = "%_v_" + this.id, cn = "%_cmp_" + this.id;
var 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`;
code += " " + cn + " = icmp ne i32 " + rn + ", 0\n";
if (errTarget) code += " br i1 " + cn + ", label %" + errTarget.id + ", label %" + (okTarget ? okTarget.id : "") + "\n";
} else if (returnStyle === "value" && io.outputs.length === 1) {
code += " " + cn + " = icmp ne i32 " + vn + ", 0\n";
if (errTarget) code += " br i1 " + cn + ", label %" + errTarget.id + ", label %" + (okTarget ? okTarget.id : "") + "\n";
}
if (okTarget) code += ` goto ${okTarget.id};\n`;
return code;
}
+91 -34
View File
@@ -1,61 +1,117 @@
// ── Pure C code generation — zero-DOM, flat DFA model ─────────────────
// ── Pure LLVM IR code generation — zero-DOM, flat DFA model ───────────
//
// 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;`.
//
// 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}] }
// Every block is an LLVM IR label. Every edge is a `br` instruction.
// Decisions become `br i1 %cond, label %yes, label %no`.
// Error handling becomes `br i1 %result, label %err, label %ok`.
(function () {
"use strict";
var CodeGen = {};
// ── String pool ─────────────────────────────────────────────────────
var _strPool = [];
CodeGen.resetStringPool = function () { _strPool = []; };
CodeGen.getStringPool = function () { return _strPool; };
/**
* 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 {Object} entry the first block (e.g. START)
* @returns {string}
* Registers a string literal and returns its unique index.
*/
CodeGen.allocString = function (s) {
var idx = _strPool.indexOf(s);
if (idx === -1) { idx = _strPool.length; _strPool.push(s); }
return idx;
};
/**
* Escapes a JS string for LLVM IR `c"..."` constant, and produces
* the full `getelementptr` reference for use as a call argument.
*/
CodeGen.llvmStringArg = function (str, idx) {
var escaped = "";
for (var i = 0; i < str.length; i++) {
var ch = str.charCodeAt(i);
if (ch === 0x22) escaped += "\\22";
else if (ch === 0x5C) escaped += "\\5C";
else if (ch === 0x0A) escaped += "\\0A";
else if (ch === 0x0D) escaped += "\\0D";
else if (ch === 0x09) escaped += "\\09";
else if (ch < 0x20 || ch > 0x7E) {
escaped += "\\" + ch.toString(16).toUpperCase().padStart(2, "0");
} else {
escaped += str[i];
}
}
var byteLen = str.length + 1; // includes null terminator
return "i8* getelementptr inbounds ([" + byteLen + " x i8], [" + byteLen + " x i8]* @.str." + idx + ", i32 0, i32 0)";
};
/**
* Generates the LLVM IR global constant for a string pool entry.
*/
CodeGen.stringGlobal = function (idx) {
var str = _strPool[idx];
var escaped = "", byteLen = 0;
for (var i = 0; i < str.length; i++) {
var ch = str.charCodeAt(i);
byteLen++;
if (ch === 0x22) escaped += "\\22";
else if (ch === 0x5C) escaped += "\\5C";
else if (ch === 0x0A) escaped += "\\0A";
else if (ch === 0x0D) escaped += "\\0D";
else if (ch === 0x09) escaped += "\\09";
else if (ch < 0x20 || ch > 0x7E) {
var h = ch.toString(16).toUpperCase();
escaped += "\\" + (h.length === 1 ? "0" : "") + h;
} else {
escaped += str[i];
}
}
byteLen++; // null terminator
return "@.str." + idx + " = private unnamed_addr constant [" + byteLen + " x i8] c\"" + escaped + "\\00\"";
};
// ── Flat DFA emit ───────────────────────────────────────────────────
CodeGen.emitFlat = function (entry) {
var visited = new Set();
var parts = [];
function bb(block) { return "bb_" + block.id; }
function emit(block) {
if (visited.has(block)) {
parts.push(" goto " + block.id + ";\n");
parts.push(" br label %" + bb(block) + "\n");
return;
}
visited.add(block);
parts.push("\n" + block.id + ":");
parts.push("\n" + bb(block) + ":");
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();
// Replace bare label references with bb_ prefixed versions
var replaced = "";
var pos = 0, m;
while ((m = /label\s+%([\w.]+)/g).lastIndex = pos, m = m.exec(fc)) {
replaced += fc.slice(pos, m.index) + "label %bb_" + m[1];
pos = m.index + m[0].length;
}
replaced += fc.slice(pos);
fc = replaced;
} else {
for (var i = 0; i < flow.length; i++) {
var t = flow[i].toElement;
if (t) fc += " goto " + t.id + ";\n";
if (t) fc += " br label %" + bb(t) + "\n";
}
}
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);
@@ -66,18 +122,19 @@
return parts.join("");
};
/**
* Assembles a complete C source file.
*/
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() {");
// ── Full module assembly ────────────────────────────────────────────
CodeGen.generateIRFile = function (entryBlock, declarations, globals, prologue) {
var lines = ["; ModuleID = \"wisecode\"", "target triple = \"x86_64-pc-linux-gnu\"", ""];
if (declarations) lines.push(declarations);
if (globals) lines.push(globals);
lines.push("");
lines.push("define i32 @main() {");
if (prologue) lines.push("entry:\n" + prologue + "\n br label %bb_" + entryBlock.id);
lines.push(CodeGen.emitFlat(entryBlock));
lines.push("}");
lines.push("// --- END ---");
lines.push("");
lines.push("; --- END ---");
return lines.join("\n");
};
+1 -1
View File
@@ -233,7 +233,7 @@ function findDataSource(hook) {
/** Copies the generated C code text to the clipboard. */
function copyCode() {
const el = $id("c-code");
const el = $id("ir-code");
if (el && el.textContent) navigator.clipboard.writeText(el.textContent).catch(() => {});
}
+3 -3
View File
@@ -446,8 +446,8 @@
<button data-action="new">New</button>
<button data-action="copy-code">Copy</button>
<button data-action="run-tests">Tests</button>
<div class="code-output" id="c-code-container">
<pre><code id="c-code"></code></pre>
<div class="code-output" id="ir-code-container">
<pre><code id="ir-code"></code></pre>
</div>
</div>
<div id="error-overlay" class="error-overlay">
@@ -527,7 +527,7 @@
<script src="persist.js"></script>
<script src="lib-loader.js"></script>
<script src="vendor/highlight/highlight.min.js"></script>
<script src="vendor/highlight/c.min.js"></script>
<script src="vendor/highlight/llvm.min.js"></script>
<script src="proc-browser.js"></script>
<script src="templates.js"></script>
<script src="app.js"></script>
+1 -3
View File
@@ -40,10 +40,8 @@ pkgs.mkShell {
opencode
python3
codegraph
sdl3
mesa # for GL/gl.h
nodejs
gcc
clang
];
shellHook = ''
+14 -14
View File
@@ -82,19 +82,19 @@ const APP_TEST_SUITE = {
this._group("Code generation", () => {
this._test("generateCode returns without error", () => {
prog.generateCode();
const out = $id("c-code");
const out = $id("ir-code");
if (!out || !out.textContent) throw Error("no code output");
});
this._test("generated code contains main function", () => {
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
if (!code.includes("int main()")) throw Error("missing int main()");
});
this._test("generated code contains return 0", () => {
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
if (!code.includes("return 0")) throw Error("missing return 0");
});
@@ -230,7 +230,7 @@ const APP_TEST_SUITE = {
_linkFlow(dec, "#hook-out1", a, ".hook-in");
_linkFlow(a, "#hook-next", end, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
dec.delete(); a.delete();
_linkFlow(start, ".hook", end, ".hook");
@@ -253,7 +253,7 @@ const APP_TEST_SUITE = {
_linkFlow(aYes, "#hook-next", end, ".hook");
_linkFlow(aNo, "#hook-next", end, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
dec.delete(); aYes.delete(); aNo.delete();
_linkFlow(start, ".hook", end, ".hook");
@@ -301,7 +301,7 @@ const APP_TEST_SUITE = {
for (const p of saved) deleteLink(p);
_linkFlow(start, ".hook", end, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
if (!code.includes("int main()")) throw Error("missing main");
if (!code.includes("return 0")) throw Error("missing return 0");
if (!code.includes("exit")) throw Error("missing exit");
@@ -530,7 +530,7 @@ const APP_TEST_SUITE = {
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); errEnd.delete();
restore();
@@ -556,7 +556,7 @@ const APP_TEST_SUITE = {
_linkFlow(okAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); errEnd.delete(); okAssign.delete();
restore();
@@ -589,7 +589,7 @@ const APP_TEST_SUITE = {
_linkFlow(sharedAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); errAssign.delete(); sharedAssign.delete();
restore();
@@ -622,7 +622,7 @@ const APP_TEST_SUITE = {
_linkFlow(okAssign, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); errEnd.delete(); dec.delete(); okAssign.delete();
restore();
@@ -641,7 +641,7 @@ const APP_TEST_SUITE = {
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete();
restore();
@@ -664,7 +664,7 @@ const APP_TEST_SUITE = {
_linkFlow(call, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); errEnd.delete();
restore();
@@ -693,7 +693,7 @@ const APP_TEST_SUITE = {
_linkFlow(call2, ".hook-out", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call1.delete(); call2.delete(); errEnd1.delete(); errEnd2.delete();
restore();
@@ -718,7 +718,7 @@ const APP_TEST_SUITE = {
_linkFlow(shared, "#hook-next", mainEnd, ".hook");
prog.generateCode();
const code = $id("c-code").textContent;
const code = $id("ir-code").textContent;
call.delete(); shared.delete();
restore();
+8 -25
View File
@@ -1,33 +1,16 @@
CC ?= gcc
CFLAGS ?= -Wall -Wextra -fsyntax-only
CLANG := clang
CFLAGS := -S -o /dev/null -x ir -Qunused-arguments
OUTDIR ?= out
SRC := $(wildcard $(OUTDIR)/*.c)
SRC := $(wildcard $(OUTDIR)/*.ll)
SDL3_CFLAGS := $(shell pkg-config --cflags sdl3 2>/dev/null || echo)
GL_CFLAGS := $(shell pkg-config --cflags gl 2>/dev/null || echo)
.PHONY: all clean
SDL3_SRC := $(filter %_sdl3.c, $(SRC))
PLAIN_SRC := $(filter-out %_sdl3.c, $(SRC))
.PHONY: all sdl3 clean
all: $(PLAIN_SRC)
@for f in $(PLAIN_SRC); do \
all: $(SRC)
@for f in $(SRC); do \
printf " CC %s\n" "$$(basename $$f)"; \
$(CC) $(CFLAGS) "$$f" 2>&1 || exit 1; \
done
sdl3: $(SDL3_SRC)
@if [ -z "$(SDL3_SRC)" ]; then \
echo " (no SDL3 tests)"; \
exit 0; \
fi
@echo " (with SDL3 flags)" ; \
for f in $(SDL3_SRC); do \
printf " CC %s\n" "$$(basename $$f)"; \
$(CC) $(CFLAGS) $(SDL3_CFLAGS) $(GL_CFLAGS) "$$f" 2>&1 || exit 1; \
$(CLANG) $(CFLAGS) "$$f" 2>&1 || exit 1; \
done
clean:
rm -f $(OUTDIR)/*.c
rm -f $(OUTDIR)/*.ll
+1 -1
View File
@@ -1 +1 @@
*.c
*.ll
+53 -65
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env node
/**
* C compilation test runner — flat DFA code generation.
* LLVM IR compilation test runner.
*
* Block data objects are minimal: generateOwnCode() + optional generateFlowCode().
* The shared CodeGen.emitFlat walks the graph, emitting labels + gotos.
* Generates .ll files from block data structures using the shared CodeGen
* module, then compiles them with `clang -S -o /dev/null` to verify valid IR.
*/
const fs = require("fs");
@@ -12,60 +12,62 @@ const { execSync } = require("child_process");
const CodeGen = require("../../codegen/index.js");
const OUTDIR = path.join(__dirname, "out");
const MKFILE = path.join(__dirname, "Makefile");
const CLANG = "clang -S -o /dev/null -x ir";
// ── Block data helpers ────────────────────────────────────────────────
function startBlock(id) {
return {
id: id,
isDecision: false,
nextFlow: [],
id: id, nextFlow: [],
generateOwnCode: function () { return ""; },
};
}
function endBlock(id, exitCode) {
var code = exitCode === 0 ? " return 0;\n" : " exit(" + exitCode + ");\n";
return {
id: id,
isDecision: false,
nextFlow: [],
generateOwnCode: function () { return code; },
id: id, nextFlow: [],
generateOwnCode: function () {
if (exitCode === 0) return " ret i32 0\n";
return " call void @exit(i32 " + exitCode + ")\n unreachable\n";
},
generateFlowCode: function () { return ""; },
};
}
function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTargets, errTargets) {
var call = procName + "(" + inputs.join(", ") + ")";
var callArgs = inputs.join(", ");
var allTargets = (okTargets || []).concat(errTargets || []);
var self = {
id: id,
isDecision: false,
nextFlow: (okTargets || []).concat(errTargets || []).map(function (t) { return { toElement: t }; }),
id: id, nextFlow: allTargets.map(function (t) { return { toElement: t }; }),
errTargets: errTargets || [],
generateOwnCode: function () {
if (returnStyle === "error" && self.errTargets.length > 0) return "";
if (returnStyle === "value" && returnValueName && self.errTargets.length > 0) {
return " " + returnValueName + " = " + call + ";\n";
if (returnStyle === "error" && self.errTargets.length > 0) {
return " %_r_" + id + " = call i32 @" + procName + "(" + callArgs + ")\n";
}
return " " + call + ";\n";
if (returnStyle === "value" && returnValueName && self.errTargets.length > 0) {
return " %_v_" + id + " = call i32 @" + procName + "(" + callArgs + ")\n";
}
return " call void @" + procName + "(" + callArgs + ")\n";
},
generateFlowCode: function () {
if (self.errTargets.length === 0) {
return self.nextFlow.map(function (p) { return " goto " + p.toElement.id + ";\n"; }).join("");
return self.nextFlow.map(function (p) { return " br label %" + p.toElement.id + "\n"; }).join("");
}
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; }
if (self.errTargets.indexOf(self.nextFlow[i].toElement) === -1) {
okT = self.nextFlow[i].toElement; break;
}
}
var out = "";
if (returnStyle === "error") {
out += " if (" + call + " != 0) goto " + errT.id + ";\n";
out += " %_cmp_" + id + " = icmp ne i32 %_r_" + id + ", 0\n";
out += " br i1 %_cmp_" + id + ", label %" + errT.id + ", label %" + (okT ? okT.id : "") + "\n";
} else if (returnStyle === "value" && returnValueName) {
out += " if (" + returnValueName + " != 0) goto " + errT.id + ";\n";
out += " %_cmp_" + id + " = icmp ne i32 %_v_" + id + ", 0\n";
out += " br i1 %_cmp_" + id + ", label %" + errT.id + ", label %" + (okT ? okT.id : "") + "\n";
}
if (okT) out += " goto " + okT.id + ";\n";
return out;
},
};
@@ -74,10 +76,8 @@ function procCallBlock(id, procName, inputs, returnStyle, returnValueName, okTar
function assignBlock(id, target, value) {
return {
id: id,
isDecision: false,
nextFlow: [],
generateOwnCode: function () { return " " + target + " = " + value + ";\n"; },
id: id, nextFlow: [],
generateOwnCode: function () { return " store i32 " + value + ", i32* %" + target + "\n"; },
};
}
@@ -91,18 +91,25 @@ 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]);
CodeGen.allocString("Hello, World!\n");
var strArg = CodeGen.llvmStringArg("Hello, World!\n", 0);
var printf = procCallBlock("printf", "printf", [strArg], "value", "result", [end], [errEnd]);
link(start, printf);
return { entry: start, includes: ["#include <stdio.h>", "#include <stdlib.h>"], declarations: " int result;" };
return {
entry: start,
decls: "declare i32 @printf(i8*, ...)\ndeclare void @exit(i32)",
globals: CodeGen.stringGlobal(0),
vars: " %r_ = alloca i32\n %v_ = alloca i32\n",
};
}
function buildErrorSimpleTemplate() {
var start = startBlock("start");
var end = endBlock("end", 0);
var errEnd = endBlock("errEnd", 1);
var call = procCallBlock("init", "func", [], "error", null, [end], [errEnd]);
var call = procCallBlock("init", "func", ["i32 0"], "error", null, [end], [errEnd]);
link(start, call);
return { entry: start, includes: ["#include <stdlib.h>"], declarations: "", proto: ["int func(void);"] };
return { entry: start, decls: "declare i32 @func(i32)\ndeclare void @exit(i32)", globals: "", vars: "" };
}
function buildErrorMergeTemplate() {
@@ -110,11 +117,11 @@ function buildErrorMergeTemplate() {
var end = endBlock("end", 0);
var logged = assignBlock("log", "logged", "1");
var done = assignBlock("done", "done", "1");
var call = procCallBlock("init", "func", [], "error", null, [done], [logged]);
var call = procCallBlock("init", "func", ["i32 0"], "error", null, [done], [logged]);
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, decls: "declare i32 @func(i32)\ndeclare void @exit(i32)", globals: "", vars: " %logged = alloca i32\n %done = alloca i32\n" };
}
function errorSequentialTemplate() {
@@ -126,47 +133,36 @@ 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, decls: "declare i32 @init_a()\ndeclare i32 @init_b()\ndeclare void @exit(i32)", globals: "", vars: "" };
}
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", ['i8* null', 'i32 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, decls: "declare i32 @open_file(i8*, i32)\ndeclare void @exit(i32)", globals: "", vars: " %fd = alloca i32\n" };
}
// ── Source generation ──────────────────────────────────────────────────
function generateSource(opts) {
var parts = ["// --- WiseCode\u00ae ---"];
for (var i = 0; i < opts.includes.length; i++) parts.push(opts.includes[i]);
if (opts.proto) {
for (var i = 0; i < opts.proto.length; i++) parts.push(opts.proto[i]);
}
parts.push("");
parts.push("int main() {");
if (opts.declarations) parts.push(opts.declarations);
parts.push(CodeGen.emitFlat(opts.entry));
parts.push("}");
parts.push("// --- END ---");
return parts.join("\n");
return CodeGen.generateIRFile(opts.entry, opts.decls, opts.globals, opts.vars);
}
// ── Main ────────────────────────────────────────────────────────────────
function writeSource(label, source) {
fs.mkdirSync(OUTDIR, { recursive: true });
var file = path.join(OUTDIR, label.replace(/[^a-z0-9]/gi, "_") + ".c");
var file = path.join(OUTDIR, label.replace(/[^a-z0-9]/gi, "_") + ".ll");
console.log(" GEN " + path.basename(file) + " (" + source.split("\n").length + " lines)");
fs.writeFileSync(file, source, "utf8");
}
console.log("\nGenerating C compilation tests...\n");
console.log("\nGenerating LLVM IR compilation tests...\n");
try { execSync("rm -f " + OUTDIR + "/*.c", { stdio: "pipe" }); } catch (_) {}
try { execSync("rm -f " + OUTDIR + "/*.ll", { stdio: "pipe" }); } catch (_) {}
writeSource("test_console", generateSource(buildConsoleTemplate()));
writeSource("test_error_simple", generateSource(buildErrorSimpleTemplate()));
@@ -174,21 +170,13 @@ writeSource("test_error_merge", generateSource(buildErrorMergeTemplate()));
writeSource("test_error_sequential", generateSource(errorSequentialTemplate()));
writeSource("test_error_value", generateSource(buildErrorValueTemplate()));
console.log("\nRunning make...\n");
console.log("\nRunning clang...\n");
try {
execSync('make -C "' + OUTDIR + '" -f "' + MKFILE + '" OUTDIR="' + OUTDIR + '" all 2>&1', { stdio: "inherit" });
execSync('make -C "' + OUTDIR + '" -f "' + path.join(__dirname, "Makefile") + '" OUTDIR="' + OUTDIR + '" all 2>&1', { stdio: "inherit" });
} catch (e) {
console.error("\n\u2717 Compilation failed.\n");
process.exit(1);
}
try {
execSync("pkg-config --exists sdl3", { stdio: "pipe" });
console.log("\nSDL3 headers found, generating SDL3 test...\n");
console.log(" (SDL3 generation not yet implemented; skipped)\n");
} catch (_) {
console.log("\nSDL3 headers not found (skipping SDL3 compilation test).\n");
}
console.log("\u2713 All compilation tests passed.\n");
console.log("\u2713 All LLVM IR compilation tests passed.\n");
+37 -44
View File
@@ -190,29 +190,27 @@ class WCProgram extends HTMLElement {
return { boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs };
}
_generateIncludes(proccalls, hasStrings) {
const libNames = new Set();
for (const p of proccalls) {
const qname = p._selectedProc;
if (qname) {
const lib = qname.split("/")[0];
if (lib) libNames.add(lib);
_generateLLVMDecls(proccalls) {
var seen = new Set();
var lines = [];
for (var pi = 0; pi < proccalls.length; pi++) {
var qname = proccalls[pi]._selectedProc;
if (!qname || seen.has(qname)) continue;
seen.add(qname);
var decl = LLVM_DECL_MAP[qname];
if (decl) {
lines.push(decl);
} else {
var fn = qname.split("/")[1] || qname;
lines.push("declare i32 @" + fn + "(...)");
}
}
let includes = Array.from(libNames).map(l => {
const path = INCLUDE_MAP[l] || (l + ".h");
return `#include <${path}>`;
}).join("\n");
if (hasStrings) {
const hdr = "#include <stdlib.h>\n#include <string.h>";
includes = includes ? includes + "\n" + hdr : hdr;
}
return includes;
return lines.join("\n");
}
generateCode() {
const outputEl = $id("c-code");
if (!outputEl) { showError("Missing #c-code output element"); return; }
const outputEl = $id("ir-code");
if (!outputEl) { showError("Missing #ir-code output element"); return; }
const { usedQuantities, usedUnits } = this._gatherUsedTypes();
@@ -220,51 +218,46 @@ class WCProgram extends HTMLElement {
const unitDefs = this._generateUnitDefs(usedUnits);
const { boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs } = this._generateVariableDeclarations();
const hasStrings = $id("variable-area").querySelectorAll("wc-string").length > 0;
const stringStruct = hasStrings
? "typedef struct { char *data; long int length; } string;\n\n"
: "";
CodeGen.resetStringPool();
const firstChild = this.firstElementChild;
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);
: "; no flowchart body\n";
// Collect external function declarations from used libraries
const proccalls = this.querySelectorAll("wc-proccall");
const decls = this._generateLLVMDecls(proccalls);
// Collect string globals
const strPool = CodeGen.getStringPool();
const globals = strPool.map(function (_, i) { return CodeGen.stringGlobal(i); }).join("\n");
// Variable declarations -> alloca instructions in the prologue
const dataSection = (boolDefs || scalarDefs || vectorDefs || stringDefs || arrayDefs)
? [boolDefs, scalarDefs, vectorDefs, stringDefs, arrayDefs]
.filter(Boolean).map(s => s + "\n").join("")
.filter(Boolean).map(function (s) { return s + "\n"; }).join("")
: "";
const hasReturnOrExit =
body.includes("\n return ") || body.includes("\n exit(");
const raw =
"// --- WiseCode\u00ae ---\n" +
(includes ? includes + "\n\n" : "") +
"int main() {\n" +
(typeDefs ? typeDefs + "\n\n" : "") +
(unitDefs ? unitDefs + "\n\n" : "") +
stringStruct + dataSection + body +
(hasReturnOrExit ? "" : "return 0;\n") +
"}\n// --- END ---";
const raw = CodeGen.generateIRFile(firstChild, decls, globals, dataSection);
outputEl.textContent = raw;
if (typeof hljs !== "undefined") hljs.highlightElement(outputEl);
outputEl.parentElement.dataset.language = "c";
outputEl.parentElement.dataset.language = "llvm";
}
}
/**
* Maps library names to their C #include paths.
* Libraries not listed here use the default `<{name}.h>` convention.
* Maps library/function names to their LLVM IR `declare` signature.
* Key is the proc qname, value is the declare line.
* Functions not listed get a generic `declare i32 @name(...)`.
*/
const INCLUDE_MAP = {
"sdl3": "SDL3/SDL.h",
"glfw": "GLFW/glfw3.h",
"gl": "GL/gl.h",
const LLVM_DECL_MAP = {
"stdio/printf": "declare i32 @printf(i8*, ...)",
"stdlib/exit": "declare void @exit(i32)",
};
// ═══════════════════════════════════════════════════════════════════════════════
// Block registry
// ═══════════════════════════════════════════════════════════════════════════════