Files
avcf/tests/compile/run.js
T
adrien 8522ece371 test: C compilation tests for generated template code
- Add gcc and mesa to shell.nix for C compilation
- Fix warning icon overlap with ::after message by adding padding-right
  to proccall-header and end-header when warnings are active
- Add tests/compile/ with Makefile (pkg-config based) and run.js
  which generates C source from block-like data structures, then
  compiles with gcc -Wall -Wextra -fsyntax-only
- Tests cover: console template, simple error, error merge,
  sequential errors, value-return error style
2026-06-23 01:01:03 +02:00

355 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* C compilation test runner.
*
* Generates C source files matching each template's expected output,
* then runs `make` to compile them with `gcc -Wall -Wextra -Werror -fsyntax-only`.
*
* Code generation is not hardcoded — each source is built from block-like
* data structures walked by the same algorithm the browser templates use.
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const OUTDIR = "/tmp/wisecode-compile";
const MKFILE = path.join(__dirname, "Makefile");
// ── Branch analysis (mirrors blocks-proc.js _branchDescendants / _emitBranch) ──
function branchDescendants(roots) {
const d = new Set(), q = [...roots];
for (const el of q) {
if (d.has(el)) continue;
d.add(el);
if (el.type === "decision") continue;
for (const p of (el.nextFlow || [])) {
if (!d.has(p.toElement)) q.push(p.toElement);
}
}
return d;
}
function emitBranch(roots, stop, visited) {
const v = new Set(visited);
const lines = [];
const walk = (el) => {
if (v.has(el) || stop.has(el)) return;
v.add(el);
if (el.type === "decision") {
lines.push(el.generateCode(v));
} else {
lines.push(el.generateOwnCode());
for (const p of (el.nextFlow || [])) walk(p.toElement);
}
};
for (const el of roots) walk(el);
return lines.join("\n");
}
// ── Block classes (simplified, match browser code gen) ────────────────
class Block {
constructor(id, type, nextFlow = []) {
this.id = id;
this.type = type;
this.nextFlow = nextFlow;
}
generateCode(visited) {
if (visited.has(this)) return "";
visited.add(this);
const own = this.generateOwnCode();
const next = this.generateNextCode(visited);
return own + next;
}
generateNextCode(visited) {
let c = "";
for (const p of this.nextFlow) {
c += p.toElement.generateCode(visited);
}
return c;
}
generateOwnCode() { return ""; }
}
class Start extends Block {
generateOwnCode() { return ""; }
}
class End extends Block {
constructor(id, code = 0) {
super(id, "end");
this.exitCode = code;
}
generateOwnCode() {
if (this.exitCode === 0) return "\n return 0;\n";
return `\n exit(${this.exitCode});\n`;
}
}
class Decision extends Block {
constructor(id, yesTargets = [], noTargets = []) {
super(id, "decision");
this.yesTargets = yesTargets;
this.noTargets = noTargets;
}
get nextFlow() {
return [...this.yesTargets, ...this.noTargets];
}
_branchDescendants(roots) {
const d = new Set(), q = [...roots];
for (const el of q) {
if (d.has(el)) continue;
d.add(el);
if (el instanceof Decision) continue;
for (const p of el.nextFlow) {
if (!d.has(p.toElement)) q.push(p.toElement);
}
}
return d;
}
_emitBranch(roots, stop, visited) {
const v = new Set(visited);
const lines = [];
const walk = (el) => {
if (v.has(el) || stop.has(el)) return;
v.add(el);
if (el instanceof Decision) {
lines.push(el.generateCode(v));
} else {
lines.push(el.generateOwnCode());
for (const p of el.nextFlow) walk(p.toElement);
}
};
for (const el of roots) walk(el);
return lines.join("\n");
}
generateCode(visited = new Set()) {
if (visited.has(this)) return "";
visited.add(this);
if (this.yesTargets.length === 0 && this.noTargets.length === 0) return "";
const yesAll = this._branchDescendants(this.yesTargets);
const noAll = this._branchDescendants(this.noTargets);
const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this));
const yesCode = this._emitBranch(this.yesTargets, shared, visited);
const noCode = this._emitBranch(this.noTargets, shared, visited);
const sharedCode = this._emitBranch([...shared], new Set(), visited);
for (const el of yesAll) visited.add(el);
for (const el of noAll) visited.add(el);
let out = "";
if (yesCode || noCode) {
if (yesCode) out += `\nif (cond) {\n${yesCode}}`;
if (noCode) {
out += yesCode
? ` else {\n${noCode}}`
: `\nif (!(cond)) {\n${noCode}}`;
}
}
out += sharedCode;
return out ? `\n${out}\n` : "";
}
}
class ProcCall extends Block {
constructor(id, procName, inputs, returnStyle, returnValueName, nextFlow = [], errTargets = []) {
super(id, "call", nextFlow);
this.procName = procName;
this.inputs = inputs;
this.returnStyle = returnStyle;
this.returnValueName = returnValueName;
this.errTargets = errTargets;
}
generateCode(visited = new Set()) {
if (visited.has(this)) return "";
visited.add(this);
const own = this.generateOwnCode();
if (this.errTargets.length > 0) {
return own;
}
return own + this.generateNextCode(visited);
}
generateOwnCode() {
const call = `${this.procName}(${this.inputs.join(", ")})`;
if (this.errTargets.length === 0) return `\n ${call};\n`;
let ifHeader;
if (this.returnStyle === "value" && this.returnValueName) {
ifHeader = ` if ((${this.returnValueName} = ${call}) != 0)`;
} else {
ifHeader = ` if (${call} != 0)`;
}
return this._generateCallWithError(ifHeader, this.errTargets);
}
_generateCallWithError(ifHeader, errTargets) {
const okTargets = this.nextFlow.map(p => p.toElement);
const errAll = branchDescendants(errTargets);
const okAll = branchDescendants(okTargets);
const shared = new Set([...errAll].filter(b => okAll.has(b)));
const errCode = emitBranch(errTargets, shared, new Set());
const okCode = emitBranch(okTargets, shared, new Set());
const sharedCode = emitBranch([...shared], new Set(), new Set());
let out = `\n${ifHeader} {\n${errCode}}\n`;
if (okCode) out += okCode;
if (sharedCode) out += sharedCode;
return out;
}
}
class Assign extends Block {
constructor(id, target, value) {
super(id, "assign");
this.target = target;
this.value = value;
}
generateOwnCode() {
return `\n ${this.target} = ${this.value};\n`;
}
}
// ── Template generators ────────────────────────────────────────────────
function buildConsoleTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const printf = new ProcCall("printf", "printf", ['"Hello, World!\\n"'], "value", "result");
const errEnd = new End("errEnd", 1);
printf.errTargets = [errEnd];
start.nextFlow.push({ toElement: printf });
printf.nextFlow.push({ toElement: end });
return { start, vars: ["int result;"], hasStdio: true, hasStdlib: true };
}
function buildErrorSimpleTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const call = new ProcCall("init", "func", [], "error", null);
const errEnd = new End("errEnd", 1);
call.errTargets = [errEnd];
start.nextFlow.push({ toElement: call });
call.nextFlow.push({ toElement: end });
return { start, vars: [], hasStdlib: true };
}
function buildErrorSimpleTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const call = new ProcCall("init", "func", [], "error", null);
const errEnd = new End("errEnd", 1);
call.errTargets = [errEnd];
start.nextFlow.push({ toElement: call });
call.nextFlow.push({ toElement: end });
return { start, vars: [], hasStdlib: true, proto: ["int func(void);"] };
}
function buildErrorMergeTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const call = new ProcCall("init", "func", [], "error", null);
const errLog = new Assign("log_err", "logged", "1");
const shared = new Assign("shared_done", "done", "1");
call.errTargets = [errLog];
start.nextFlow.push({ toElement: call });
errLog.nextFlow.push({ toElement: shared });
call.nextFlow.push({ toElement: shared });
shared.nextFlow.push({ toElement: end });
return { start, vars: ["int logged;", "int done;"],
hasStdlib: true, proto: ["int func(void);"] };
}
function errorSequentialTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const call1 = new ProcCall("a", "init_a", [], "error", null);
const call2 = new ProcCall("b", "init_b", [], "error", null);
const errEnd1 = new End("err1", 1);
const errEnd2 = new End("err2", 2);
call1.errTargets = [errEnd1];
call2.errTargets = [errEnd2];
start.nextFlow.push({ toElement: call1 });
call1.nextFlow.push({ toElement: call2 });
call2.nextFlow.push({ toElement: end });
return { start, vars: [], hasStdlib: true, proto: ["int init_a(void);", "int init_b(void);"] };
}
function buildErrorValueTemplate() {
const start = new Start("start");
const end = new End("end", 0);
const call = new ProcCall("open", "open_file", ['"data.txt"', "0"], "value", "fd");
const errEnd = new End("errEnd", 1);
call.errTargets = [errEnd];
start.nextFlow.push({ toElement: call });
call.nextFlow.push({ toElement: end });
return { start, vars: ["int fd;"], hasStdlib: true, proto: ["int open_file(const char *path, int flags);"] };
}
function generateSource({ start, vars, hasStdio, hasStdlib, proto }) {
const includes = [];
if (hasStdio) includes.push("#include <stdio.h>");
if (hasStdlib) includes.push("#include <stdlib.h>");
const body = start.generateCode(new Set());
const code = [
"// --- WiseCode\u00ae ---",
...includes,
...(proto ? proto : []),
"",
"int main() {",
...(vars ? vars.map(v => " " + v) : []),
body,
"}",
"// --- END ---",
].join("\n");
return code;
}
// ── Main ────────────────────────────────────────────────────────────────
function writeSource(label, source) {
fs.mkdirSync(OUTDIR, { recursive: true });
const file = path.join(OUTDIR, label.replace(/[^a-z0-9]/gi, "_") + ".c");
/* Show a short preview */
const firstLines = source.split("\n").slice(0, 4).join("\n");
console.log(` GEN ${path.basename(file)} (${source.split("\n").length} lines)`);
fs.writeFileSync(file, source, "utf8");
}
console.log("\nGenerating C compilation tests...\n");
// Clear old files
try { execSync(`rm -f ${OUTDIR}/*.c`, { stdio: "pipe" }); } catch (_) {}
writeSource("test_console", generateSource(buildConsoleTemplate()));
writeSource("test_error_simple", generateSource(buildErrorSimpleTemplate()));
writeSource("test_error_merge", generateSource(buildErrorMergeTemplate()));
writeSource("test_error_sequential", generateSource(errorSequentialTemplate()));
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",
});
} 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");
// Future: generate SDL3 template C source and run `make sdl3`
// writeSource("test_sdl3", generateSDL3Source());
// execSync("make ... sdl3", ...);
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");