Files
scriptc/packages/compiler/test/emit-llvm.test.ts
T
Malte UblandClaude Opus 5.5 89bca68cbe Compute with NaN-boxed jsval values in the dynamic tier
Stage S3 of the two-tier design: dynamic-tier functions compute with
jsval, a new IR type holding a 64-bit NaN-boxed word whose numbers,
booleans, undefined and null are immediates, so a number between two
dynamic operators is no longer a heap node.

- IR: the jsval type and js.* builtins (operators and the boundary
  conversions fromDyn/toDyn/fromF64/fromBool/fromStr/fromBigInt/toF64).
  The jsval pass rewrites dynamic-tier functions after tier assignment:
  operator chains, comparisons, number checks and truthiness of operator
  results run on jsvals; dyn locals become jsval locals when that saves
  boxing; dynamic functions called only from dynamic functions return a
  jsval. IR version 17 serializes each function's tier.
- Runtime: scr_js.h and scr_js_value.c behind the dynTier feature
  (invariant I3); static runtime units never include scr_js.h.
- Backend: number cases inline (immediate doubles and NUM cells), always
  inlined jsval retain/release; 32-bit targets keep boxed cells.
- Interprocedural analyses see jsval conversions as the dyn forms they
  replace, so no fact about a static function changes.

Static functions are untouched: typed programs and the self-hosted
compiler's static functions compile to byte-identical LLVM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:11:32 -07:00

286 lines
8.5 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
import { compileC } from "../src/backend/native-toolchain.js";
import { validateModule } from "../src/ir/validate.js";
import { fibModule } from "./fixtures/fib-ir.js";
import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule } from "../src/ir/ir.js";
const execFileAsync = promisify(execFile);
async function emitCompileRun(mod: IrModule, sanitize = true): Promise<string> {
expect(validateModule(mod)).toEqual([]);
const dir = await mkdtemp(join(tmpdir(), "scriptc-emit-"));
const cPath = join(dir, "program.ll");
await writeFile(cPath, emitLlvmModule(mod));
const outPath = join(dir, "program");
await compileC({ cPath, outPath, sanitize });
const { stdout } = await execFileAsync(outPath);
return stdout;
}
test("hand-built fib IR compiles and prints 55", async () => {
expect(await emitCompileRun(fibModule)).toBe("55\n");
});
test("strings: literals, concat in a loop, toString, RC-clean under audit", async () => {
const loc = { file: "s.ts", start: 0, end: 0 };
const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
// let acc = "x"; let i = 0;
// while (i < 3) { acc = acc + ("-" + i); i = i + 1; }
// console.log(acc, acc === "x-0-1-2", "α∂" < "β");
const mod: IrModule = {
irVersion: 17,
sourceFile: "s.ts",
entry: "__main",
functions: [
{
name: "__main",
params: [],
returnType: VOID,
locals: [
{ id: "acc.0", name: "acc", type: STRING, mutable: true },
{ id: "i.0", name: "i", type: F64, mutable: true },
],
body: [
{ kind: "varDecl", localId: "acc.0", init: str("x"), loc },
{ kind: "varDecl", localId: "i.0", init: num(0), loc },
{
kind: "while",
cond: {
kind: "bin",
op: "<",
left: { kind: "varRef", localId: "i.0", type: F64, loc },
right: num(3),
type: BOOL,
loc,
},
body: [
{
kind: "assign",
localId: "acc.0",
value: {
kind: "strConcat",
left: { kind: "varRef", localId: "acc.0", type: STRING, loc },
right: {
kind: "strConcat",
left: str("-"),
right: {
kind: "toString",
operand: { kind: "varRef", localId: "i.0", type: F64, loc },
type: STRING,
loc,
},
type: STRING,
loc,
},
type: STRING,
loc,
},
loc,
},
{
kind: "assign",
localId: "i.0",
value: {
kind: "bin",
op: "+",
left: { kind: "varRef", localId: "i.0", type: F64, loc },
right: num(1),
type: F64,
loc,
},
loc,
},
],
loc,
},
{
kind: "exprStmt",
expr: {
kind: "intrinsic",
name: "console.log",
args: [
{ kind: "varRef", localId: "acc.0", type: STRING, loc },
{
kind: "strEq",
negated: false,
left: { kind: "varRef", localId: "acc.0", type: STRING, loc },
right: str("x-0-1-2"),
type: BOOL,
loc,
},
{ kind: "strCmp", op: "<", left: str("α∂"), right: str("β"), type: BOOL, loc },
],
type: VOID,
loc,
},
loc,
},
],
loc,
},
],
};
expect(await emitCompileRun(mod)).toBe("x-0-1-2 true true\n");
});
test("short-circuit: right operand of && only evaluates when left is true", async () => {
const loc = { file: "l.ts", start: 0, end: 0 };
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
// function sideEffect(): boolean { console.log("evaluated"); return true; }
// if (false && sideEffect()) {} ; if (true || sideEffect()) {}
// console.log("done")
const mod: IrModule = {
irVersion: 17,
sourceFile: "l.ts",
entry: "__main",
functions: [
{
name: "sideEffect",
params: [],
returnType: BOOL,
locals: [],
body: [
{
kind: "exprStmt",
expr: {
kind: "intrinsic",
name: "console.log",
args: [{ kind: "strLit", value: "evaluated", type: STRING, loc }],
type: VOID,
loc,
},
loc,
},
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
],
loc,
},
{
name: "__main",
params: [],
returnType: VOID,
locals: [],
body: [
{
kind: "if",
cond: {
kind: "logical",
op: "&&",
left: { kind: "boolLit", value: false, type: BOOL, loc },
right: { kind: "call", callee: "sideEffect", args: [], type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: { kind: "intrinsic", name: "console.log", args: [num(1)], type: VOID, loc },
loc,
},
],
else_: null,
loc,
},
{
kind: "if",
cond: {
kind: "logical",
op: "||",
left: { kind: "boolLit", value: true, type: BOOL, loc },
right: { kind: "call", callee: "sideEffect", args: [], type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: {
kind: "intrinsic",
name: "console.log",
args: [{ kind: "strLit", value: "done", type: STRING, loc }],
type: VOID,
loc,
},
loc,
},
],
else_: null,
loc,
},
],
loc,
},
],
};
expect(await emitCompileRun(mod)).toBe("done\n");
});
test("string params: callee owns and releases; returns transfer ownership", async () => {
const loc = { file: "p.ts", start: 0, end: 0 };
// function greet(who: string): string { return "hi " + who; }
// console.log(greet("world"));
const mod: IrModule = {
irVersion: 17,
sourceFile: "p.ts",
entry: "__main",
functions: [
{
name: "greet",
params: [{ localId: "who.0", name: "who", type: STRING }],
returnType: STRING,
locals: [{ id: "who.0", name: "who", type: STRING, mutable: false }],
body: [
{
kind: "return",
value: {
kind: "strConcat",
left: { kind: "strLit", value: "hi ", type: STRING, loc },
right: { kind: "varRef", localId: "who.0", type: STRING, loc },
type: STRING,
loc,
},
loc,
},
],
loc,
},
{
name: "__main",
params: [],
returnType: VOID,
locals: [],
body: [
{
kind: "exprStmt",
expr: {
kind: "intrinsic",
name: "console.log",
args: [
{
kind: "call",
callee: "greet",
args: [{ kind: "strLit", value: "world", type: STRING, loc }],
type: STRING,
loc,
},
],
type: VOID,
loc,
},
loc,
},
],
loc,
},
],
};
expect(await emitCompileRun(mod)).toBe("hi world\n");
});