feat(compiler): bootstrap the LLVM emitter natively

- Run the production LLVM backend natively and reproduce its output across compiler generations.
- Replace unchecked emitter contexts with explicit shared state and callback hosts.
- Preserve NaN in serialized IR and respect optional record storage during spreads.
This commit is contained in:
Chris Tate
2026-09-27 22:29:24 -05:00
committed by GitHub
parent 37a25ebf7f
commit 7b007e03ef
50 changed files with 1053 additions and 614 deletions
+6
View File
@@ -62,6 +62,7 @@ jobs:
--exclude=tests/harness/vercel-e2e.test.ts
--exclude=tests/harness/self-hosting-serialization.test.ts
--exclude=tests/harness/self-hosting.test.ts
--exclude=tests/harness/self-hosting-llvm-emitter.test.ts
--exclude=tests/harness/fetch.test.ts
--exclude=tests/harness/npm-static.test.ts
--exclude=tests/harness/surface-manifest.test.ts
@@ -76,6 +77,11 @@ jobs:
env:
SCRIPTC_TEST_WORKERS: "1"
run: pnpm test tests/harness/self-hosting.test.ts
- name: Tests (self-hosting LLVM emitter)
if: matrix.shard == 2
env:
SCRIPTC_TEST_WORKERS: "1"
run: pnpm test tests/harness/self-hosting-llvm-emitter.test.ts
# These long files otherwise collide in one Vitest file shard and contend
# for the worker RPC. Spread them across jobs with one worker each.
- name: Tests (fetch)
@@ -9,7 +9,7 @@ function fn(name: string, body: IrStmt[], locals: IrLocal[] = [local]): IrFuncti
return { name, body, locals, params: [], returnType: VOID, loc };
}
function moduleWith(...functions: IrFunction[]): IrModule {
return { irVersion: 12, sourceFile: loc.file, entry: "caller", functions };
return { irVersion: 13, sourceFile: loc.file, entry: "caller", functions };
}
const assignment: IrStmt = { kind: "assign", localId: local.id, value, loc };
const expression: IrExpr = { kind: "assignExpr", localId: local.id, value, type: F64, loc };
@@ -6,7 +6,7 @@ import { computeTraced as llvmTraced } from "./llvm/shapes.js";
const loc = { file: "cycles.ts", start: 0, end: 1 };
function module(): IrModule {
return { irVersion: 12, sourceFile: loc.file, entry: "main", functions: [{ name: "main", locals: [], params: [], returnType: VOID, body: [], loc }] };
return { irVersion: 13, sourceFile: loc.file, entry: "main", functions: [{ name: "main", locals: [], params: [], returnType: VOID, body: [], loc }] };
}
const ref = (shapeId: string): IrType => ({ kind: "record", shapeId });
const shape = (id: string, types: IrType[]): IrRecordShape => ({ id, fields: types.map((type, i) => ({ name: `field${i}`, type })) });
+6 -5
View File
@@ -1145,13 +1145,14 @@ export class LlDyn {
const lCache = B.newLabel("dcu.put");
B.condBr(hasCached, lRefresh, lCache);
B.startBlock(lRefresh);
([
const fields: [number, string, string][] = [
[1, "i32", "tag"],
[2, "ptr", "retain"],
[3, "ptr", "release"],
[4, "ptr", "trace"],
[5, "i64", "slot"],
] as const).forEach(([index, fieldType, fieldName]) => {
];
fields.forEach(([index, fieldType, fieldName]) => {
const cachedPtr = B.tmp();
const checkedPtr = B.tmp();
const oldValue = B.tmp();
@@ -1755,9 +1756,9 @@ export class LlDyn {
for (const k of [DYN_KIND.NULL, DYN_KIND.BOOL, DYN_KIND.NUM, DYN_KIND.STR, DYN_KIND.ARR, DYN_KIND.OBJ, DYN_KIND.UNDEF, DYN_KIND.BYTES, DYN_KIND.FUNC, DYN_KIND.HANDLE, DYN_KIND.PROMISE, DYN_KIND.JSVAL, DYN_KIND.TYPED_REF]) {
labels.set(k, B.newLabel(`ds.k${k}`));
}
B.terminate(
`switch i32 ${kd}, label %${done} [ ${[...labels].map(([k, l]) => `i32 ${k}, label %${l}`).join(" ")} ]`,
);
const branches: string[] = [];
for (const [kind, label] of labels) branches.push(`i32 ${kind}, label %${label}`);
B.terminate(`switch i32 ${kd}, label %${done} [ ${branches.join(" ")} ]`);
B.startBlock(labels.get(DYN_KIND.JSVAL)!);
{
// Island-held: the engine's own ToString (a bridged failure
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { IrType, isRefCounted, isUnitType, typeEquals } from "../../ir/ir.js";
import { type IrType, isRefCounted, isUnitType, typeEquals } from "../../ir/ir.js";
import { mangleRecordNew } from "../mangle.js";
import { arrNewCall, traceAdapter, traceArg, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -67,7 +67,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
B.line(`${len} = call double @scr_arr_len(ptr ${ps.name})`);
B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`);
const vals = B.tmp();
B.line(`${vals} = ${arrNewCall(host, elem, cap)}`);
B.line(`${vals} = ${arrNewCall(host.shapeHost, elem, cap)}`);
host.own({ name: vals, type: e.type.inner });
const t = B.tmp();
B.line(`${t} = call ptr @scr_promise_all(ptr ${ps.name}, ptr ${vals}, ptr @${store})`);
@@ -124,11 +124,11 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
B.line(`call void @scr_promise_fulfill_str(ptr ${p}, ptr ${v.name})`);
} else {
const rc = vAdapters(host, t);
const rc = vAdapters(host.shapeHost, t);
host.moveTemp(v);
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
B.line(
`call void @scr_promise_fulfill_ref(ptr ${p}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, t)})`,
`call void @scr_promise_fulfill_ref(ptr ${p}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
);
}
return out;
@@ -229,7 +229,7 @@ export function emitSerializationExpr(host: LlvmEmitterContext, e: ExprOf<"jsonS
// TypeError mid-walk: finish still runs (frees the buffer, the
// partial string joins the frame and releases on unwind), then
// the pending check unwinds — the C emitter's contract exactly.
if (traceAdapter(host, e.value.type) !== null) host.emitPendingCheck();
if (traceAdapter(host.shapeHost, e.value.type) !== null) host.emitPendingCheck();
}
// A pretty-print form (`stringify(v, null, 2)`): the frontend
// resolved the space to a compile-time indent string (Node's
@@ -318,10 +318,10 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
host.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`);
const g = B.tmp();
B.line(`${g} = call ptr @scr_gen_of_fiber(ptr ${coro.self})`);
B.line(`call void @scr_gen_out_ref(ptr ${g}, ptr ${v.name}, ptr ${vAdapters(host, yt).release})`);
B.line(`call void @scr_gen_out_ref(ptr ${g}, ptr ${v.name}, ptr ${vAdapters(host.shapeHost, yt).release})`);
} else {
host.declare(`declare void @scr_gen_yield_ref(ptr, ptr)`);
B.line(`call void @scr_gen_yield_ref(ptr ${v.name}, ptr ${vAdapters(host, yt).release})`);
B.line(`call void @scr_gen_yield_ref(ptr ${v.name}, ptr ${vAdapters(host.shapeHost, yt).release})`);
}
}
if (host.wasi) host.emitWasiSuspendPrepared();
@@ -370,7 +370,7 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
B.line(`call void @scr_gen_in_bool(ptr ${g.name}, i1 ${name})`);
} else {
host.declare(`declare void @scr_gen_in_ref(ptr, ptr, ptr)`);
B.line(`call void @scr_gen_in_ref(ptr ${g.name}, ptr ${name}, ptr ${vAdapters(host, t).release})`);
B.line(`call void @scr_gen_in_ref(ptr ${g.name}, ptr ${name}, ptr ${vAdapters(host.shapeHost, t).release})`);
}
};
if (genT.async) {
@@ -403,7 +403,7 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
call = `call ptr @scr_async_gen_next_bool(ptr ${g.name}, i1 ${a.name})`;
} else {
host.declare(`declare ptr @scr_async_gen_next_ref(ptr, ptr, ptr)`);
call = `call ptr @scr_async_gen_next_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host, t).release})`;
call = `call ptr @scr_async_gen_next_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host.shapeHost, t).release})`;
}
}
} else if (e.mode === "return") {
@@ -422,7 +422,7 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
call = `call ptr @scr_async_gen_return_bool(ptr ${g.name}, i1 ${a.name})`;
} else {
host.declare(`declare ptr @scr_async_gen_return_ref(ptr, ptr, ptr)`);
call = `call ptr @scr_async_gen_return_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host, t).release})`;
call = `call ptr @scr_async_gen_return_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host.shapeHost, t).release})`;
}
}
} else {
@@ -474,7 +474,7 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
B.line(`call void @scr_gen_ret_bool(ptr ${g.name}, i1 ${name})`);
} else {
host.declare(`declare void @scr_gen_ret_ref(ptr, ptr, ptr)`);
B.line(`call void @scr_gen_ret_ref(ptr ${g.name}, ptr ${name}, ptr ${vAdapters(host, t).release})`);
B.line(`call void @scr_gen_ret_ref(ptr ${g.name}, ptr ${name}, ptr ${vAdapters(host.shapeHost, t).release})`);
}
});
}
@@ -730,7 +730,8 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" |
B.line(`${rec} = call ptr @${mangleRecordNew(e.type.shapeId)}()`);
const out = host.own({ name: rec, type: e.type });
// The three +1s move straight into the fresh record's fields.
for (const [field, value] of [["promise", p], ["resolve", resolve], ["reject", reject]] as const) {
const fields: [string, string][] = [["promise", p], ["resolve", resolve], ["reject", reject]];
for (const [field, value] of fields) {
const { ptr } = host.recordFieldPtr(rec, e.type.shapeId, field);
B.line(`store ptr ${value}, ptr ${ptr}`);
}
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { F64, IrBytesElem, IrExpr } from "../../ir/ir.js";
import { F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { F64_INF, f64Lit } from "./common.js";
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
import { type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import { FN_ATTRS, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -33,10 +33,10 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
if (tag < 0) throw new InternalCompilerError("llvm emitter bug: race adapter arm missing (frontend must fence)");
return tag;
};
const rv = vAdapters(host, to);
const rv = vAdapters(host.shapeHost, to);
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
const fulfill = (value: string): string =>
`call void @scr_promise_fulfill_ref(ptr %dst, ptr ${value}, ptr ${rv.retain}, ptr ${rv.release}, ptr ${traceArg(host, to)})`;
`call void @scr_promise_fulfill_ref(ptr %dst, ptr ${value}, ptr ${rv.retain}, ptr ${rv.release}, ptr ${traceArg(host.shapeHost, to)})`;
const d: string[] = [
`define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`,
`entry:`,
@@ -62,12 +62,12 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
host.declare(`declare void @scr_str_release_v(ptr)`);
} else {
const fv = vAdapters(host, from);
const fv = vAdapters(host.shapeHost, from);
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
d.push(
` %x = call ptr @scr_promise_payload_ref(ptr %src)`,
` %u = call ptr @scr_union_new_ref(i32 ${tag}, ptr %x, ptr ${fv.retain}, ptr ${fv.release}, ptr ${traceArg(host, from)})`,
` %u = call ptr @scr_union_new_ref(i32 ${tag}, ptr %x, ptr ${fv.retain}, ptr ${fv.release}, ptr ${traceArg(host.shapeHost, from)})`,
);
}
d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``);
@@ -111,13 +111,13 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
` br label %join`,
);
} else {
const av = vAdapters(host, arm);
const av = vAdapters(host.shapeHost, arm);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
d.push(
` %pp${i} = getelementptr inbounds %ScrUnion, ptr %u0, i64 0, i32 5`,
` %p${i} = load ptr, ptr %pp${i}`,
` %r${i} = call ptr ${av.retain}(ptr %p${i})`,
` %v${i} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %r${i}, ptr ${av.retain}, ptr ${av.release}, ptr ${traceArg(host, arm)})`,
` %v${i} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %r${i}, ptr ${av.retain}, ptr ${av.release}, ptr ${traceArg(host.shapeHost, arm)})`,
` store ptr %v${i}, ptr %slot`,
` br label %join`,
);
@@ -242,11 +242,11 @@ export function genResultThunkFor(host: LlvmEmitterContext, genT: IrType & { kin
}
host.declare(`declare ptr @scr_gen_take_out_ref(ptr)`);
if (srcT.kind !== "union") {
const v = vAdapters(host, srcT);
const v = vAdapters(host.shapeHost, srcT);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
return [
` %${px}x = call ptr @scr_gen_take_out_ref(ptr %g)`,
` %${px}u = call ptr @scr_union_new_ref(i32 ${tagOf(srcT)}, ptr %${px}x, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, srcT)})`,
` %${px}u = call ptr @scr_union_new_ref(i32 ${tagOf(srcT)}, ptr %${px}x, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host.shapeHost, srcT)})`,
` store ptr %${px}u, ptr %vslot`,
` br label %join`,
];
@@ -291,13 +291,13 @@ export function genResultThunkFor(host: LlvmEmitterContext, genT: IrType & { kin
` store ptr %${px}v${i}, ptr %vslot`,
);
} else {
const av = vAdapters(host, arm);
const av = vAdapters(host.shapeHost, arm);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
lines.push(
` %${px}pp${i} = getelementptr inbounds %ScrUnion, ptr %${px}u0, i64 0, i32 5`,
` %${px}p${i} = load ptr, ptr %${px}pp${i}`,
` %${px}r${i} = call ptr ${av.retain}(ptr %${px}p${i})`,
` %${px}v${i} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %${px}r${i}, ptr ${av.retain}, ptr ${av.release}, ptr ${traceArg(host, arm)})`,
` %${px}v${i} = call ptr @scr_union_new_ref(i32 ${tag}, ptr %${px}r${i}, ptr ${av.retain}, ptr ${av.release}, ptr ${traceArg(host.shapeHost, arm)})`,
` store ptr %${px}v${i}, ptr %vslot`,
);
}
@@ -456,7 +456,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
if (errorTag < 0 || nullTag < 0 || errorArm === null) {
throw new InternalCompilerError("llvm emitter bug: execFile error union lacks its arms");
}
errorTrace = traceArg(host, errorArm);
errorTrace = traceArg(host.shapeHost, errorArm);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
host.declare(`declare void @scr_error_release_v(ptr)`);
@@ -525,7 +525,7 @@ export function ipcMessageThunkFor(host: LlvmEmitterContext, cbT: IrType & { kin
` %bad = call zeroext i1 @scr_exc_pending()`,
` br i1 %bad, label %fail, label %invoke`,
`fail:`,
...(isRefCounted(param) ? [` call void ${releaseSym(host, param)}(ptr %value)`] : []),
...(isRefCounted(param) ? [` call void ${releaseSym(host.shapeHost, param)}(ptr %value)`] : []),
` ret void`,
`invoke:`,
);
@@ -540,7 +540,7 @@ export function ipcMessageThunkFor(host: LlvmEmitterContext, cbT: IrType & { kin
if (retTy === "void") d.push(` call void %fn(ptr %cb${passed})`);
else {
d.push(` %result = call ${retTy} %fn(ptr %cb${passed})`);
if (isRefCounted(cbT.ret)) d.push(` call void ${releaseSym(host, cbT.ret)}(ptr %result)`);
if (isRefCounted(cbT.ret)) d.push(` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %result)`);
}
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
@@ -583,7 +583,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
` %has = icmp ne ptr %error, null`,
` br i1 %has, label %some, label %none`,
`some:`,
` %wrapped = call ptr @scr_union_new_ref(i32 ${errorTag}, ptr %error, ptr @scr_error_retain_v, ptr @scr_error_release_v, ptr ${traceArg(host, errorArm)})`,
` %wrapped = call ptr @scr_union_new_ref(i32 ${errorTag}, ptr %error, ptr @scr_error_retain_v, ptr @scr_error_release_v, ptr ${traceArg(host.shapeHost, errorArm)})`,
` store ptr %wrapped, ptr %slot`,
` br label %invoke`,
`none:`,
@@ -662,7 +662,7 @@ export function emitterFixedAdapter(host: LlvmEmitterContext, cbT: IrType & { ki
d.push(` %b${i} = load i1, ptr %a${i}`);
passed.push(`i1 %b${i}`);
} else if (isRefCounted(p)) {
d.push(` %r${i} = call ptr ${retainSym(host, p)}(ptr %a${i})`);
d.push(` %r${i} = call ptr ${retainSym(host.shapeHost, p)}(ptr %a${i})`);
passed.push(`ptr %r${i}`);
} else {
passed.push(`ptr %a${i}`);
@@ -678,7 +678,7 @@ export function emitterFixedAdapter(host: LlvmEmitterContext, cbT: IrType & { ki
} else {
d.push(` %ret = call ${retTy} %fn(${passed.join(", ")})`);
if (isRefCounted(cbT.ret)) {
d.push(` call void ${releaseSym(host, cbT.ret)}(ptr %ret) ; discarded listener result`);
d.push(` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`);
}
}
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``);
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
import { IrExpr, IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { mangleResolveThunk } from "../mangle.js";
import { elemAccess, FN_ATTRS, mapKeyAccess, mapKeyKindNum, mapValKindNum, traceArg, vAdapters } from "./shapes.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
@@ -13,12 +13,12 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
if (!sym) {
sym = mangleResolveThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
const v = vAdapters(host, inner);
const v = vAdapters(host.shapeHost, inner);
host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`);
host.resolveThunkDefs.push(
`define internal void @${sym}(ptr %self, ptr %v) ${FN_ATTRS} { ; resolve<${key}>`,
`entry:`,
` call void @scr_resolve_ref_impl(ptr %self, ptr %v, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host, inner)})`,
` call void @scr_resolve_ref_impl(ptr %self, ptr %v, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(host.shapeHost, inner)})`,
` ret void`,
`}`,
``,
@@ -506,16 +506,16 @@ export function emitMapNew(host: LlvmEmitterContext, e: IrExpr & { kind: "mapNew
if (e.type.kind !== "map") throw new InternalCompilerError("llvm emitter bug: mapNew of non-map type");
const B = host.B;
const value = e.type.value;
const rc = isRefCounted(value) ? vAdapters(host, value) : { retain: "null", release: "null" };
const rc = isRefCounted(value) ? vAdapters(host.shapeHost, value) : { retain: "null", release: "null" };
const m = B.tmp();
const kAcc = mapKeyAccess(e.type.key);
if (kAcc === "ref") {
const keyRc = vAdapters(host, e.type.key);
const keyRc = vAdapters(host.shapeHost, e.type.key);
host.declare(`declare ptr @scr_map_new_typed(i32, i32, ptr, ptr, ptr, ptr, ptr, ptr)`);
B.line(`${m} = call ptr @scr_map_new_typed(i32 ${mapKeyKindNum(e.type.key)}, i32 ${mapValKindNum(value)}, ptr ${keyRc.retain}, ptr ${keyRc.release}, ptr ${traceArg(host, e.type.key)}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, value)})`);
B.line(`${m} = call ptr @scr_map_new_typed(i32 ${mapKeyKindNum(e.type.key)}, i32 ${mapValKindNum(value)}, ptr ${keyRc.retain}, ptr ${keyRc.release}, ptr ${traceArg(host.shapeHost, e.type.key)}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, value)})`);
} else {
host.declare(`declare ptr @scr_map_new(i32, i32, ptr, ptr, ptr)`);
B.line(`${m} = call ptr @scr_map_new(i32 ${mapKeyKindNum(e.type.key)}, i32 ${mapValKindNum(value)}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, value)})`);
B.line(`${m} = call ptr @scr_map_new(i32 ${mapKeyKindNum(e.type.key)}, i32 ${mapValKindNum(value)}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, value)})`);
}
const out = host.own({ name: m, type: e.type });
// Seeded construction: set() each pair in source order — a repeated
@@ -729,9 +729,9 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew
const kAcc = mapKeyAccess(e.type.elem);
const s = B.tmp();
if (kAcc === "ref") {
const rc = vAdapters(host, e.type.elem);
const rc = vAdapters(host.shapeHost, e.type.elem);
host.declare(`declare ptr @scr_map_new_typed(i32, i32, ptr, ptr, ptr, ptr, ptr, ptr)`);
B.line(`${s} = call ptr @scr_map_new_typed(i32 ${mapKeyKindNum(e.type.elem)}, i32 0, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, e.type.elem)}, ptr null, ptr null, ptr null)`);
B.line(`${s} = call ptr @scr_map_new_typed(i32 ${mapKeyKindNum(e.type.elem)}, i32 0, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, e.type.elem)}, ptr null, ptr null, ptr null)`);
} else {
host.declare(`declare ptr @scr_map_new(i32, i32, ptr, ptr, ptr)`);
B.line(`${s} = call ptr @scr_map_new(i32 ${mapKeyKindNum(e.type.elem)}, i32 0, ptr null, ptr null, ptr null)`);
@@ -1,15 +1,10 @@
/* Internal type-only boundary between the LLVM emitter core and its
* extracted expression/library emission modules. Runtime ownership stays
* with LlEmitter; helpers receive this structural view at delegation. */
import type { IrBytesElem, IrExpr, IrFfiImport, IrFunction, IrLibFn, IrLocal, IrModule, IrRecordShape, IrStmt, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js";
import type { FfiCallbackAdapter } from "../ffi-callbacks.js";
import type { ConstantNumericTable } from "../../ir/constant-tables.js";
import type { IntegerRanges } from "../../ir/integer-ranges.js";
import type { BlockBuilder } from "./blocks.js";
import type { LlClassMeta } from "./classes.js";
import type { LlDyn } from "./dyn.js";
import type { ShapeHost } from "./shapes.js";
import type { LlWalkers } from "./walkers.js";
/* Expression helpers share the actual emitter instance. A nominal context
* preserves mutable state and checked method signatures without casting a
* class to a record of closures. Layout stages use explicit callback hosts. */
import type { IrExpr, IrLibFn, IrType } from "../../ir/ir.js";
import type { LlEmitter } from "./emitter.js";
export type LlvmEmitterContext = LlEmitter;
export interface LlValue {
name: string;
@@ -34,169 +29,3 @@ export interface LlStreamTypedRefContext {
adapters: Map<string, LlStreamTypedRefAdapter>;
unions?: Map<string, string>;
}
export interface LlvmEmitterContext extends ShapeHost {
B: BlockBuilder;
abiOffset(native64: number, wasm32: number): number;
arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string;
binding(id: string): { kind: "global" | "local" | "boxed"; slot: string; type: IrType; local?: IrLocal };
boxGet(box: string, t: IrType): string;
boxSet(box: string, t: IrType, value: string): void;
callTarget(fnName: string): string;
chainSlots: Map<string, LlValue>;
childDataThunkFor(param: IrType): string;
childExitSignalThunkFor(codeParam: IrType, sigParam: IrType): string;
childExitThunkFor(param: IrType): string;
execFileThunkFor(cbT: IrType & { kind: "func" }): string;
ipcMessageThunkFor(cbT: IrType & { kind: "func" }): string;
ipcSendThunkFor(cbT: IrType & { kind: "func" }): string;
classFieldPtr(objName: string, className: string, field: string): { ptr: string; type: IrType };
classMeta: Map<string, LlClassMeta>;
classMetaOf(className: string): LlClassMeta;
classObjSym(className: string): string;
closeBindThunkFor(cbUnion: IrType, retServer: boolean): string;
closeOverrideWrapFor(cbUnion: IrType, retServer: boolean): string;
cstr(text: string): string;
currentGenerator: { yieldT: IrType; nextT: IrType; } | null;
constantNumericTables: ReadonlyMap<string, ConstantNumericTable>;
integerRanges: IntegerRanges;
currentWasiCoro: { kind: "async" | "generator"; id: string; handle: string; self: string; finalLabel: string; cleanupLabel: string; suspendLabel: string; } | null;
declare(decl: string): void;
dyn: LlDyn;
dynKind(d: string): string;
dynPromiseAdapter(inner: IrType): string;
dynPromiseAdapters: Map<string, string>;
emitArrIntrinsic(e: IrExpr & { kind: "arrIntrinsic" }): LlValue;
emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void;
emitAssertInspectLibCall(e: LibCallExpr): LlValue;
emitAsyncContextLibCall(e: LibCallExpr): LlValue;
emitAsyncExpr(e: ExprOf<"yieldExpr" | "genResume" | "awaitExpr" | "awaitUnionExpr" | "newPromise" | "promiseWithResolvers">): LlValue;
emitBranchInto(slot: string, expr: IrExpr): void;
emitBytesData(receiver: string): string;
emitBytesGet(elem: IrBytesElem, receiver: string, index: string, integerIndex?: boolean): LlValue;
emitBytesIndex(receiver: string, index: string, integerIndex?: boolean): string;
emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue;
emitBytesLength(elem: IrBytesElem, receiver: string, bytes: boolean): LlValue;
emitStableReceiver(receiver: IrExpr, following: IrExpr[]): LlValue;
emitToUint32(value: string, expr?: IrExpr): string;
emitCallExpr(e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">): LlValue;
emitChildProcessLibCall(e: LibCallExpr): LlValue;
emitContainerExpr(e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">): LlValue;
emitControlExpr(e: ExprOf<"dynDestrCheck" | "dynIterN" | "toBool" | "logical" | "ternary" | "optChain" | "chainRecv" | "orDefault" | "nullish">): LlValue;
emitDynamicExpr(e: ExprOf<"dynFrom" | "dynFromJsval" | "dynCall" | "dynInvoke" | "dynArrLit" | "dynObjLit" | "unionWrap" | "unionNarrow" | "unionDisc" | "unionKeyGet" | "unionIsTag" | "dynKeyGet" | "dynHasKey" | "dynScalarEq" | "dynTest" | "unionEq" | "unionFuncEq" | "caughtTest" | "caughtCheck" | "caughtNarrow" | "caughtToDyn">): LlValue;
emitDynamicLibCall(e: LibCallExpr): LlValue;
emitErrorsEventsLibCall(e: LibCallExpr): LlValue;
emitExpr(e: IrExpr): LlValue;
emitFilesystemLibCall(e: LibCallExpr): LlValue;
emitGenericLibCall(e: LibCallExpr): LlValue;
emitIntegerLoopIndex(expr: IrExpr): string | null;
emitIntrinsicExpr(e: ExprOf<"intrinsic">): LlValue;
emitIoLibCall(e: LibCallExpr): LlValue;
emitJsExit(e: IrExpr & { kind: "jsExit" }): LlValue;
emitJsInteropExpr(e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">): LlValue;
emitJsMarshal(e: IrExpr & { kind: "jsMarshal" }): LlValue;
emitJsOp(e: IrExpr & { kind: "jsOp" }): LlValue;
emitLibCall(e: LibCallExpr): LlValue;
emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue;
emitMapLikeIntrinsic(e: Extract<IrExpr, { kind: "mapIntrinsic" | "setIntrinsic" }>): LlValue;
emitMapNew(e: IrExpr & { kind: "mapNew" }): LlValue;
emitNetworkHttpLibCall(e: LibCallExpr): LlValue;
emitOperatorExpr(e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">): LlValue;
emitPathUrlLibCall(e: LibCallExpr): LlValue;
emitPendingCheck(): void;
emitPrimitiveLibCall(e: LibCallExpr): LlValue;
emitProcessLibCall(e: LibCallExpr): LlValue;
emitRecordExpr(e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">): LlValue;
emitRecordKeyGet(e: IrExpr & { kind: "recordKeyGet" }): LlValue;
emitRegexIntrinsic(e: IrExpr & { kind: "regexIntrinsic" }): LlValue;
emitSerializationExpr(e: ExprOf<"jsonStringify" | "dynCheck">): LlValue;
emitSetNew(e: IrExpr & { kind: "setNew" }): LlValue;
emitStmt(s: IrStmt): void;
emitStrIntrinsic(e: IrExpr & { kind: "strIntrinsic" }): LlValue;
emitStreamLibCall(e: LibCallExpr): LlValue;
emitStringExpr(e: ExprOf<"strConcat" | "strEq" | "strCmp" | "toString" | "strIntrinsic" | "regexLit" | "templateStrings" | "regexIntrinsic">): LlValue;
emitStringSelfConcatAssign(localId: string, left: IrExpr, suffix: IrExpr, retainForYield: boolean): LlValue;
emitThrowValue(v: LlValue): void;
emitWasiSuspend(promise: string | null): void;
emitWasiSuspendPrepared(): void;
emitWebLibCall(e: LibCallExpr): LlValue;
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string };
ffiByName: Map<string, IrFfiImport>;
ffiCallbackAdapter(binding: string, id: string): FfiCallbackAdapter;
ffiHasRetainedCallback: boolean;
fnByName: Map<string, IrFunction>;
fnValues: Set<string>;
frames: LlValue[][];
fsRenameThunkFor(cbT: IrType & { kind: "func" }): string;
cryptoBytesThunkFor(cbT: IrType & { kind: "func" }): string;
zlibBytesThunkFor(cbT: IrType & { kind: "func" }): string;
genResultThunkFor(genT: IrType & { kind: "generator" }, recT: IrType & { kind: "record" }): string;
indirectMayThrow: boolean;
integerLoopBindings: Map<string, string>;
internLiteral(text: string): string;
islandAdapter(arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string;
islandTypedAdapter(fn: IrType & { kind: "func" }): string;
keyedRecordReadInto(slot: string, join: string, objName: string, keyName: string, shapeId: string, resultType: IrType, overflowOnly: boolean, loc?: SrcLoc): void;
liveDynRefAdapters: Map<string, LlStreamTypedRefAdapter>;
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
liveDynUnionRefAdapters: Map<string, string>;
llType(t: IrType): string;
loadBox(slot: string): string;
loadField(ptr: string, t: IrType): string;
loadVtPre(objName: string, staticClassName: string): string;
logArgSlots: number;
mapSet(m: string, kAcc: "f64" | "str" | "ref", vAcc: "f64" | "bool" | "ref", key: string, value: string): void;
mayThrow: Set<string>;
mayThrowMethods: Set<string>;
mod: IrModule;
moveTemp(v: LlValue): void;
needsBadKey: boolean;
needsBadTag: boolean;
own(v: LlValue): LlValue;
ownSlot(slot: string, type: IrType): void;
raceAdapterFor(from: IrType, to: IrType): string;
recordCloneShapes: Set<string>;
recordFieldPtr(objName: string, shapeId: string, field: string): { ptr: string; type: IrType };
recordOvfPtr(objName: string, shapeId: string): string;
recordShape(shapeId: string): IrRecordShape;
recordsById: Map<string, IrRecordShape>;
regexInstances: Map<string, { sym: string; src: string; fl: string; }>;
releaseFrame(frame: LlValue[]): void;
releaseValue(name: string, type: IrType): void;
resolveThunkDefs: string[];
resolveThunkFor(inner: IrType): string;
resolveThunks: Map<string, string>;
retainBox(box: string): string;
retainValue(name: string, type: IrType): string;
sizeType: "i32" | "i64";
ffiExtendNarrowIntegers: boolean;
storeField(ptr: string, t: IrType, value: string): void;
streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string;
streamDataAdapter(cbT: IrType & { kind: "func" }): string;
streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string;
streamFromArrayAdapter(t: IrType & { kind: "array" }): string;
streamFromArrayAdapters: Map<string, string>;
streamTypedRefBoxValue(B: BlockBuilder, t: IrType, value: string, ctx: LlStreamTypedRefContext): string;
streamTypedRefCommitAdapter(t: IrType, snapshot: string): string;
streamTypedRefMaterializeAdapter(t: IrType, ctx: LlStreamTypedRefContext, preferredSnapshot?: string): LlStreamTypedRefAdapter;
tagInSet(uName: string, tags: number[]): string;
tdzBoxRead(box: string, t: IrType, name: string): string;
checkGlobalTdz(id: string): void;
writeBindingBox(box: string, local: IrLocal, value: string, initializes?: boolean, borrowed?: boolean): void;
templateStringsInstances: Map<string, { sym: string; slots: string[]; }>;
truthy(v: LlValue): string;
unionExtract(uName: string, arm: IrType): string;
unionNewOwned(tag: number, v: LlValue): string;
unionPeek(uName: string): string;
unionTag(uName: string): string;
unionTagSwitch(uName: string, def: IrUnionDef, arm: (armType: IrType, tag: number) => void): void;
unionsById: Map<string, IrUnionDef>;
unitInstanceRef(unionId: string, tag: number): string;
unwrapNullableClosure(u: string, funcTag: number): string;
usesTimers: boolean;
walkers: LlWalkers;
wasi: boolean;
wrapEmitterListener(target: string, adapterFn: string): string;
wrapNullable(raw: string, present: string, valueType: IrType, valueTag: number, resultType: IrType & { kind: "union" }, absentTag: number): LlValue;
}
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { IrExpr } from "../../ir/ir.js";
import { type IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
export function emitJsInteropExpr(host: LlvmEmitterContext, e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">): LlValue {
@@ -56,7 +56,7 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
);
host.liveDynRefAdapters.set(key, adapter);
}
const rc = vAdapters(host, v.type);
const rc = vAdapters(host.shapeHost, v.type);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
);
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { canMarshalFuncIntoIsland, IrExpr, IrType, islandCallbackRet, islandPromisePayloadTag, isRefCounted, isUnitType, STRING, typeKey } from "../../ir/ir.js";
import { canMarshalFuncIntoIsland, type IrExpr, type IrType, islandCallbackRet, islandPromisePayloadTag, isRefCounted, isUnitType, STRING, typeKey } from "../../ir/ir.js";
import { FN_ATTRS, releaseSym } from "./shapes.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
@@ -565,7 +565,7 @@ export function islandTypedAdapter(host: LlvmEmitterContext, fn: IrType & { kind
if (isRefCounted(p)) {
cleanup.push(
` %cf${i} = load ptr, ptr %sl${i}`,
` call void ${releaseSym(host, p)}(ptr %cf${i})`,
` call void ${releaseSym(host.shapeHost, p)}(ptr %cf${i})`,
);
}
});
@@ -769,12 +769,12 @@ export function islandTypedAdapter(host: LlvmEmitterContext, fn: IrType & { kind
` %rpend = call zeroext i1 @scr_exc_pending()`,
` br i1 %rpend, label %jfail, label %jok`,
`jfail:`,
...(isRefCounted(fn.ret) ? [` call void ${releaseSym(host, fn.ret)}(${host.llType(fn.ret)} %rv)`] : []),
...(isRefCounted(fn.ret) ? [` call void ${releaseSym(host.shapeHost, fn.ret)}(${host.llType(fn.ret)} %rv)`] : []),
` ret ptr null`,
`jok:`,
` call void @scr_jb_init(ptr %jbuf)`,
` call void @${helper}(ptr %jbuf, ${host.llType(fn.ret)} %rv)`,
...(isRefCounted(fn.ret) ? [` call void ${releaseSym(host, fn.ret)}(${host.llType(fn.ret)} %rv)`] : []),
...(isRefCounted(fn.ret) ? [` call void ${releaseSym(host.shapeHost, fn.ret)}(${host.llType(fn.ret)} %rv)`] : []),
` %rj = call ptr @scr_jb_finish(ptr %jbuf)`,
` %j = call ptr @scr_jsval_from_json(ptr %rj)`,
` call void @scr_str_release(ptr %rj)`,
@@ -17,7 +17,7 @@ function fixture(): IrModule {
loc,
});
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals: [], body, loc }],
@@ -441,7 +441,7 @@ export function emitContainerExpr(host: LlvmEmitterContext, e: ExprOf<"arrayLit"
if (e.type.kind !== "array") throw new InternalCompilerError("llvm emitter bug: arrayLit of non-array type");
const elem = e.type.elem;
const arr = B.tmp();
B.line(`${arr} = ${arrNewCall(host, elem, String(e.elems.length))}`);
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(e.elems.length))}`);
const out = host.own({ name: arr, type: e.type });
const acc = elemAccess(elem);
const spreadSet = new Set(e.spreads ?? []);
@@ -466,7 +466,7 @@ export function emitContainerExpr(host: LlvmEmitterContext, e: ExprOf<"arrayLit"
const elem = e.type.elem;
const n = host.emitExpr(e.length);
const arr = B.tmp();
B.line(`${arr} = ${arrNewCall(host, elem, "0")}`);
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, "0")}`);
const out = host.own({ name: arr, type: e.type });
const acc = elemAccess(elem);
let fill = acc === "f64" ? f64Lit(0) : acc === "bool" ? "false" : "null";
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag, unionWideningTags } from "../../ir/analysis.js";
import { IrExpr, IrType, isRefCounted, SrcLoc, typeEquals } from "../../ir/ir.js";
import { type IrExpr, type IrType, isRefCounted, type SrcLoc, typeEquals } from "../../ir/ir.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { f64Lit, llvmCommentText } from "./common.js";
@@ -1,7 +1,7 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { IrType, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { type IrType, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { classFieldIndex, classStructSym } from "./classes.js";
@@ -45,10 +45,10 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
B.line(`call void @scr_promise_fulfill_str(ptr %dst, ptr ${value})`);
} else {
const rc = vAdapters(host, inner);
const rc = vAdapters(host.shapeHost, inner);
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
B.line(
`call void @scr_promise_fulfill_ref(ptr %dst, ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, inner)})`,
`call void @scr_promise_fulfill_ref(ptr %dst, ptr ${value}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, inner)})`,
);
}
B.terminate(`ret void`);
@@ -181,7 +181,7 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
lines.push(` store ${fieldTy} %${next}, ptr %f${index}_ptr`);
}
if (isRefCounted(field.type)) {
lines.push(` call void ${releaseSym(host, field.type)}(ptr %f${index}_old)`);
lines.push(` call void ${releaseSym(host.shapeHost, field.type)}(ptr %f${index}_old)`);
}
lines.push(` br label %${after}`, `${after}:`);
}
@@ -231,7 +231,7 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
);
});
lines.push(
` call void ${releaseSym(host, t)}(ptr %next)`,
` call void ${releaseSym(host.shapeHost, t)}(ptr %next)`,
` br label %done`,
`done:`,
` ret void`,
@@ -301,7 +301,7 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
);
mutableArms.forEach(({ arm, tag }, index) => {
const adapter = adapters.get(tag)!;
const rc = vAdapters(host, arm);
const rc = vAdapters(host.shapeHost, arm);
const armKey = typeKey(arm);
B.startBlock(armLabels[index]!);
const payloadPtr = B.tmp();
@@ -376,7 +376,8 @@ function nestedTypedRefUnionAdapter(
ctx: LlStreamTypedRefContext,
): string {
const key = typeKey(t);
const unions = (ctx.unions ??= new Map());
if (ctx.unions === undefined) ctx.unions = new Map<string, string>();
const unions = ctx.unions;
const existing = unions.get(key);
if (existing) return existing;
const def = host.unionsById.get(t.unionId);
@@ -404,7 +405,7 @@ function nestedTypedRefUnionAdapter(
B.startBlock(labels[index]!);
if (streamTypedRefEligible(arm) || isDynTypedRefType(arm)) {
const adapter = host.streamTypedRefMaterializeAdapter(arm, ctx);
const rc = vAdapters(host, arm);
const rc = vAdapters(host.shapeHost, arm);
const armKey = typeKey(arm);
const payloadPtr = B.tmp();
const payload = B.tmp();
@@ -495,7 +496,7 @@ export function streamTypedRefBoxValue(host: LlvmEmitterContext,
return boxed;
}
const nested = host.streamTypedRefMaterializeAdapter(t, ctx);
const rc = vAdapters(host, t);
const rc = vAdapters(host.shapeHost, t);
const key = typeKey(t);
host.declare(
`declare ptr @scr_dyn_new_typed_ref(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr, ptr)`,
@@ -651,7 +652,7 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
const boxed = host.streamTypedRefBoxValue(B, elem, value, ctx);
B.line(`call void @scr_dyn_arr_push(ptr ${out}, ptr ${boxed})`);
if (isRefCounted(elem)) {
B.line(`call void ${releaseSym(host, elem)}(ptr ${value})`);
B.line(`call void ${releaseSym(host.shapeHost, elem)}(ptr ${value})`);
}
});
B.terminate(`ret ptr ${out}`);
@@ -750,7 +751,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
arm,
armSnapshot,
);
const rc = vAdapters(host, arm);
const rc = vAdapters(host.shapeHost, arm);
B.startBlock(armLabels[i]!);
const payloadPtr = B.tmp();
const payload = B.tmp();
@@ -786,7 +787,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
}
} else if (typedRef) {
boxed = B.tmp();
const rc = vAdapters(host, elem);
const rc = vAdapters(host.shapeHost, elem);
const keyPtr = host.cstr(key);
let commit: string;
if (streamTypedRefEligible(elem)) {
@@ -819,7 +820,7 @@ export function streamFromArrayAdapter(host: LlvmEmitterContext,
);
}
if (isRefCounted(elem)) {
B.line(`call void ${releaseSym(host, elem)}(ptr ${value})`);
B.line(`call void ${releaseSym(host.shapeHost, elem)}(ptr ${value})`);
}
B.terminate(`ret ptr ${boxed}`);
host.resolveThunkDefs.push(
@@ -1,6 +1,6 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { IrType, isRefCounted, typeKey } from "../../ir/ir.js";
import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js";
import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js";
import type { LlvmEmitterContext } from "./expr-context.js";
@@ -35,7 +35,7 @@ export function streamDataAdapter(host: LlvmEmitterContext, cbT: IrType & { kind
} else {
d.push(` %ret = call ${retTy} %fn(${argList})`);
if (isRefCounted(cbT.ret)) {
d.push(` call void ${releaseSym(host, cbT.ret)}(ptr %ret) ; discarded listener result`);
d.push(` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`);
}
}
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`);
@@ -57,7 +57,7 @@ export function streamDataAdapter(host: LlvmEmitterContext, cbT: IrType & { kind
` call void @scr_closure_release(ptr %orig)`,
` ret void`,
`ok:`,
` %r0 = call ptr ${retainSym(host, p)}(ptr ${slot})`,
` %r0 = call ptr ${retainSym(host.shapeHost, p)}(ptr ${slot})`,
);
finish("%r0");
d.push(`}`, ``);
@@ -563,7 +563,7 @@ export function streamCbThunkFor(host: LlvmEmitterContext, kind: "r" | "w" | "f"
` br i1 %euh${i}, label %euy${i}, label %eun${i}`,
`euy${i}:`,
` %eur${i} = call ptr @scr_error_retain_v(ptr %err)`,
` %euu${i} = call ptr @scr_union_new_ref(i32 ${errTag}, ptr %eur${i}, ptr @scr_error_retain_v, ptr @scr_error_release_v, ptr ${traceArg(host, def!.arms[errTag]!)})`,
` %euu${i} = call ptr @scr_union_new_ref(i32 ${errTag}, ptr %eur${i}, ptr @scr_error_retain_v, ptr @scr_error_release_v, ptr ${traceArg(host.shapeHost, def!.arms[errTag]!)})`,
` store ptr %euu${i}, ptr %euslot${i}`,
` br label %eug${i}`,
`eun${i}:`,
@@ -593,7 +593,7 @@ export function streamCbThunkFor(host: LlvmEmitterContext, kind: "r" | "w" | "f"
} else {
d.push(` %ret = call ${retTy} %fn(${passed.join(", ")})`);
if (isRefCounted(cbT.ret)) {
d.push(` call void ${releaseSym(host, cbT.ret)}(ptr %ret) ; discarded option-callback result`);
d.push(` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded option-callback result`);
}
}
d.push(` ret void`, `}`, ``);
@@ -228,11 +228,11 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
host.storeField(host.recordFieldPtr(rec, inner.shapeId, countField).ptr, F64, count);
const payload = host.retainValue(args[1]!.name, e.args[1]!.type);
B.line(`store ptr ${payload}, ptr ${host.recordFieldPtr(rec, inner.shapeId, "buffer").ptr}`);
const rc = vAdapters(host, inner);
const rc = vAdapters(host.shapeHost, inner);
host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`);
const result = B.tmp();
B.line(
`${result} = call ptr @scr_promise_settled_ref(ptr ${rec}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, inner)})`,
`${result} = call ptr @scr_promise_settled_ref(ptr ${rec}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, inner)})`,
);
return host.own({ name: result, type: e.type });
}
@@ -327,7 +327,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
const cnt = B.tmp();
B.line(`${cnt} = call ${host.sizeType} @scr_fs_scandir_count(ptr ${snap})`);
const arr = B.tmp();
B.line(`${arr} = ${arrNewCall(host, recT, cnt)}`);
B.line(`${arr} = ${arrNewCall(host.shapeHost, recT, cnt)}`);
const arrayOut = host.own({ name: arr, type: arrayT });
const iSlot = B.slot();
B.entryAllocas.push(`${iSlot} = alloca ${host.sizeType}`);
@@ -360,12 +360,12 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.startBlock(le);
B.line(`call void @scr_fs_scandir_free(ptr ${snap})`);
if (!promiseForm) return arrayOut;
const rc = vAdapters(host, arrayT);
const rc = vAdapters(host.shapeHost, arrayT);
host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`);
host.moveTemp(arrayOut); // promise fulfillment owns the result array
const promise = B.tmp();
B.line(
`${promise} = call ptr @scr_promise_settled_ref(ptr ${arr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arrayT)})`,
`${promise} = call ptr @scr_promise_settled_ref(ptr ${arr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, arrayT)})`,
);
return host.own({ name: promise, type: e.type });
}
@@ -499,12 +499,13 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
const cidrNullTag = cidrDef.arms.findIndex((a) => a.kind === "nullT");
const r = B.tmp();
B.line(`${r} = call ptr @${mangleRecordNew(t.shapeId)}()`);
for (const [field, sym] of [
const fields: [string, string][] = [
["address", "scr_os_ifaddrs_address"],
["netmask", "scr_os_ifaddrs_netmask"],
["family", "scr_os_ifaddrs_family"],
["mac", "scr_os_ifaddrs_mac"],
] as const) {
];
for (const [field, sym] of fields) {
const v = B.tmp();
B.line(`${v} = call ptr @${sym}(ptr ${snap}, ${host.sizeType} ${i}) ; +1`);
B.line(`store ptr ${v}, ptr ${host.recordFieldPtr(r, t.shapeId, field).ptr}`);
@@ -548,9 +549,9 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
B.line(`${su} = call ptr @scr_union_retain_v(ptr ${host.unitInstanceRef(st.unionId, undefTag)})`);
B.line(`store ptr ${su}, ptr ${host.recordFieldPtr(r, t.shapeId, "scopeid").ptr}`);
}
const rc = vAdapters(host, t);
const rc = vAdapters(host.shapeHost, t);
const rowU = B.tmp();
B.line(`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, t)})`);
B.line(`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`);
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
B.br(lRow);
};
@@ -582,12 +583,12 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
B.br(lj);
B.startBlock(lm);
const fresh = B.tmp();
B.line(`${fresh} = ${arrNewCall(host, infoT, "1")}`);
const arrRc = vAdapters(host, arrT);
B.line(`${fresh} = ${arrNewCall(host.shapeHost, infoT, "1")}`);
const arrRc = vAdapters(host.shapeHost, arrT);
const freshRet = B.tmp();
B.line(`${freshRet} = call ptr @scr_arr_retain_v(ptr ${fresh})`);
const bucketU = B.tmp();
B.line(`${bucketU} = call ptr @scr_union_new_ref(i32 ${arrTag}, ptr ${freshRet}, ptr ${arrRc.retain}, ptr ${arrRc.release}, ptr ${traceArg(host, arrT)})`);
B.line(`${bucketU} = call ptr @scr_union_new_ref(i32 ${arrTag}, ptr ${freshRet}, ptr ${arrRc.retain}, ptr ${arrRc.release}, ptr ${traceArg(host.shapeHost, arrT)})`);
B.line(`call void @scr_map_set_str_ref(ptr ${ovf}, ptr ${nm}, ptr ${bucketU})`);
B.line(`store ptr ${fresh}, ptr ${rowsSlot}`);
B.br(lj);
@@ -180,10 +180,11 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (!def) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union unknown`);
const args = e.args.map((a) => host.emitExpr(a));
const pushing = e.fn === "readable.pushU";
const entries: Record<string, string> = pushing
const entries: Record<"bytes" | "string" | "nullT", string> = pushing
? { bytes: "scr_stream_push", string: "scr_stream_push_str", nullT: "scr_stream_push_null" }
: { bytes: "scr_stream_write", string: "scr_stream_write_str", nullT: "scr_stream_write_null" };
const present = (["nullT", "string", "bytes"] as const)
const kinds: ("nullT" | "string" | "bytes")[] = ["nullT", "string", "bytes"];
const present = kinds
.map((kind) => ({ kind, tag: def.arms.findIndex((a) => a.kind === kind) }))
.filter((a) => a.tag >= 0);
if (present.length === 0) throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
@@ -220,7 +221,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
// The C shape is a ternary chain ending at the LAST present arm
// (no default): mirror with a tag switch whose default is that arm.
const last = present[present.length - 1]!;
const labels = present.slice(0, -1).map((a) => ({ ...a, label: B.newLabel(`scu.${a.kind}`) }));
const labels = present.slice(0, -1).map((a) => ({ kind: a.kind, tag: a.tag, label: B.newLabel(`scu.${a.kind}`) }));
const ld = B.newLabel("scu.d");
if (labels.length > 0) {
B.terminate(
@@ -342,6 +342,15 @@ if (srcShape && shapeHasAccessorSlots(srcShape)) {
);
}
}
/** Only definitely present later records replace earlier defaults. A
* callback binding may store undefined despite its checker annotation. */
function laterSpreadType(lowerer: Lowerer, expr: ts.Expression): IrType | null {
let source = expr;
while (ts.isParenthesizedExpression(source)) source = source.expression;
if (ts.isIdentifier(source)) return lowerer.lowerExpr(source).type;
return lowerer.mapTypeOf(lowerer.typeOf(source));
}
/** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
* lambda whose body fails to lower inside a JS object literal becomes a
* closure of the field's exact func type whose body is the runtime fence —
@@ -1188,8 +1197,7 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
// equal type (a wider source would silently DROP fields JS keeps —
// the width fence, same as literals). Later contributors override
// earlier ones (JS last-write-wins; the reads are side-effect-free,
// so dropping the earlier read is exact). Identifier sources
// re-read per field (historic path); any OTHER source must be a
// so dropping the earlier read is exact). Sources must be a
// re-emittable pure read, sharing one lowered node per field.
// The desugar's one-entry-per-name list reads spread fields
// EAGERLY at the spread's position, so an explicit property
@@ -1211,10 +1219,14 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
const srcLowered =
prop === expr.properties[0] && leadingSpreadLowered !== null
? leadingSpreadLowered
: ts.isIdentifier(srcNode)
? null
: lowerer.lowerExpr(srcNode);
const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
: lowerer.lowerExpr(srcNode);
// Array callbacks can receive a record-or-undefined ABI even when
// TypeScript describes a required record. Select the copy strategy
// from that stored representation so absent sources copy nothing.
// Checked-dynamic bindings still use the checker's field contract.
const srcType = srcLowered.type.kind === "dyn"
? lowerer.mapTypeOf(lowerer.typeOf(srcNode))
: srcLowered.type;
// `...options.installConfig` — a spread of `Partial<X> | undefined`
// (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
// JS spreads nothing for the unit arm and copies present keys
@@ -1272,7 +1284,7 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
if (ts.isSpreadAssignment(later)) {
if (conditionalSpreadOf(later.expression)) continue;
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
const lt = laterSpreadType(lowerer, later.expression);
if (lt?.kind === "record") {
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? []) laterNames.add(lf.name);
}
@@ -1410,7 +1422,7 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
if (ts.isSpreadAssignment(later)) {
if (conditionalSpreadOf(later.expression)) continue;
const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
const lt = laterSpreadType(lowerer, later.expression);
if (lt?.kind === "record") {
for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? []) laterNames.add(lf.name);
}
@@ -3745,7 +3745,7 @@ export class Lowerer {
this.diags.length > 0
? null
: {
irVersion: 12,
irVersion: 13,
sourceFile: this.entry.fileName,
functions,
classes: artifacts.classes,
@@ -13,7 +13,7 @@ const expr = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: value, loc })
function fixture(): IrModule {
return {
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
globals: [{ id, name: "table", type, mutable: false }],
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [
{ kind: "assign", localId: id, value: { kind: "arrayLit", elems: [num(2), num(-0), num(Infinity)], type, loc }, loc },
+1 -1
View File
@@ -801,7 +801,7 @@ export function isRefCounted(t: IrType): boolean {
export interface IrModule {
/** Bumped on any breaking IR change; serialize.ts refuses mismatches. */
irVersion: 12;
irVersion: 13;
sourceFile: string;
functions: IrFunction[];
/** Class shapes. Constructors and methods are ordinary module functions
@@ -12,7 +12,7 @@ const read = (localId: string, field = "a"): IrExpr => ({ kind: "recordGet", obj
function fixture(): IrModule {
return {
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
records: [{ id: "r0", fields: [{ name: "a", type: F64 }, { name: "b", type: F64 }] }],
functions: [
{
@@ -0,0 +1,47 @@
import { expect, test } from "vitest";
import { F64, VOID, type IrModule } from "./ir.js";
import { IR_VERSION, deserializeModule, serializeModule } from "./serialize.js";
function numbers(values: number[]): IrModule {
const loc = { file: "numbers.ts", start: 0, end: 0 };
return {
irVersion: IR_VERSION, sourceFile: loc.file, entry: "main",
functions: [{
name: "main", params: [], locals: [], returnType: VOID, loc,
body: values.map((value) => ({ kind: "exprStmt", expr: { kind: "numLit", value, type: F64, loc }, loc })),
}],
};
}
test("IR numbers survive multiple serialized generations without losing their value", () => {
const values = [NaN, Infinity, -Infinity, -0, 0, Number.MIN_VALUE, Number.MAX_VALUE, -Number.MIN_VALUE, 0.1 + 0.2];
let mod = numbers(values);
const first = serializeModule(mod);
expect(first).toContain('"$nonfinite": "nan"');
expect(first).toContain('"$nonfinite": "-0"');
for (let generation = 0; generation < 3; generation++) {
mod = deserializeModule(serializeModule(mod));
const actual = mod.functions[0]!.body.map((stmt) => {
if (stmt.kind !== "exprStmt" || stmt.expr.kind !== "numLit") throw new Error("lost numeric literal");
return stmt.expr.value;
});
expect(actual.every((value, index) => Object.is(value, values[index]))).toBe(true);
expect(serializeModule(mod)).toBe(first);
}
});
test.each(["", "NaN", "infinity", "future-format"])("unknown number sentinel %j is refused", (tag) => {
const json = serializeModule(numbers([NaN])).replace('"nan"', JSON.stringify(tag));
expect(() => deserializeModule(json)).toThrow("Invalid IR number sentinel");
});
test("ordinary strings resembling number sentinels remain strings", () => {
const mod = numbers([0]);
mod.sourceFile = '{"$nonfinite":"nan"}';
expect(deserializeModule(serializeModule(mod)).sourceFile).toBe(mod.sourceFile);
});
test("the new number format rejects documents bearing an older version", () => {
const json = serializeModule(numbers([NaN])).replace(`"irVersion": ${IR_VERSION}`, `"irVersion": ${IR_VERSION - 1}`);
expect(() => deserializeModule(json)).toThrow("IR version mismatch");
});
+11 -15
View File
@@ -1,23 +1,15 @@
import { InternalCompilerError } from "../errors.js";
/* IR ↔ JSON. The IR is plain JSON-safe data by construction; the value of
* this module is the version fence and the finiteness assertion (numLit
* holds a JS number — NaN/Infinity literals cannot appear in source, but
* a frontend bug producing one must not silently become `null` in JSON).
*/
/* IR ↔ JSON. Numeric literals preserve JavaScript's complete number domain:
* JSON's null/zero spellings must not erase NaN, infinities or negative zero. */
import type { IrModule } from "./ir.js";
// Discriminators choose native record layouts at checked JSON boundaries.
// Older consumers must not ignore them and fall back to structural matching.
export const IR_VERSION = 12 as const;
// Version 13 adds the NaN sentinel. Older readers interpret unknown tags
// as negative infinity, so accepting a newer document would miscompile it.
export const IR_VERSION = 13 as const;
export function serializeModule(mod: IrModule): string {
return JSON.stringify(mod, (_key, value) => {
if (typeof value === "number" && !Number.isFinite(value)) {
// ±Infinity numLits are real (the global `Infinity`); JSON cannot
// spell them, so they ride a sentinel object no other IR value can
// be (numbers never serialize as objects). NaN stays a bug.
if (Number.isNaN(value)) throw new InternalCompilerError("IR contains NaN; refusing to serialize");
return { $nonfinite: value > 0 ? "inf" : "-inf" };
return { $nonfinite: Number.isNaN(value) ? "nan" : value > 0 ? "inf" : "-inf" };
}
// JSON.stringify(-0) prints "0", silently losing the sign a numLit's
// f64 semantics depend on (String(-0) is "0" but 1/-0 is -Infinity) —
@@ -33,7 +25,11 @@ export function deserializeModule(json: string): IrModule {
const mod = JSON.parse(json, (_key, value: unknown) => {
if (typeof value === "object" && value !== null && "$nonfinite" in value) {
const tag = (value as { $nonfinite: string }).$nonfinite;
return tag === "inf" ? Infinity : tag === "-0" ? -0 : -Infinity;
if (tag === "inf") return Infinity;
if (tag === "-inf") return -Infinity;
if (tag === "-0") return -0;
if (tag === "nan") return NaN;
throw new Error(`Invalid IR number sentinel: ${String(tag)}`);
}
return value;
}) as IrModule;
+4 -4
View File
@@ -35,14 +35,14 @@ function numericReadModule(overrides: Partial<IrExpr & { kind: "arrIntrinsic" }>
type: F64, loc, ...overrides,
};
return {
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [{ kind: "exprStmt", expr: read, loc }], loc }],
};
}
function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
return {
irVersion: 12, sourceFile: loc.file, entry: "main", unions,
irVersion: 13, sourceFile: loc.file, entry: "main", unions,
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [{ kind: "exprStmt", expr, loc }], loc }],
};
}
@@ -196,7 +196,7 @@ test.each([
function tdzModule(mutable = true): IrModule {
const value: IrExpr = { kind: "numLit", value: 0, type: F64, loc };
return {
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
functions: [{
name: "main", params: [], returnType: VOID, loc,
locals: [{ id: "value", name: "value", type: F64, mutable, boxed: true, tdz: true }],
@@ -313,7 +313,7 @@ test("TDZ locals require a shared box", () => {
function discriminatedModule(): IrModule {
return {
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
records: [
{ id: "empty", fields: [{ name: "kind", type: STRING }] },
@@ -65,7 +65,7 @@ const sink = (name: string): IrFunction => ({
/** A module holding the case function plus the two declared sinks. */
function caseModule(params: string[], locals: string[], body: IrStmt[]): IrModule {
return {
irVersion: 12,
irVersion: 13,
sourceFile: "corpus.ts",
functions: [
sink("send"),
@@ -136,7 +136,7 @@ const RECORD_CFG: IntSlotConfig = {
function recordCase(body: IrStmt[], names = ["m"], extraFns: IrFunction[] = []): IrModule {
return {
irVersion: 12,
irVersion: 13,
sourceFile: "fields.ts",
functions: [
...extraFns,
@@ -177,7 +177,7 @@ const classCountRead = (): IrExpr => ({
function onlyOrdinaryClass(body: IrStmt[]): IntVerdict {
const mod: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "class-fields.ts",
functions: [
sink("send"),
@@ -391,7 +391,7 @@ describe("the domain's edges beyond the corpus", () => {
loc,
};
const mod: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "optional.ts",
functions: [{
name: "normalize",
@@ -250,7 +250,7 @@ test("semantic library cache restores and rebases IR after a comment-only edit",
const sourceBefore = await readFile(f.source, "utf8");
const returnStart = sourceBefore.indexOf("return");
const semanticMod = {
irVersion: 12,
irVersion: 13,
sourceFile: f.source,
functions: [{
name: "__main",
@@ -308,7 +308,7 @@ test("semantic library cache refuses token and directive edits", async () => {
const f = await fixture();
const sourceBefore = await readFile(f.source, "utf8");
const semanticMod = {
irVersion: 12,
irVersion: 13,
sourceFile: f.source,
functions: [{
name: "__main",
@@ -353,7 +353,7 @@ test("semantic C cache accepts only line-preserving single-source edits", async
const f = await fixture();
const sourceBefore = await readFile(f.source, "utf8");
const semanticMod = {
irVersion: 12,
irVersion: 13,
sourceFile: f.source,
functions: [{
name: "__main",
@@ -424,7 +424,7 @@ test("semantic C cache refuses non-LF separator normalization", async () => {
frontend: tracker.snapshot(),
semantic: {
mod: {
irVersion: 12,
irVersion: 13,
sourceFile: f.source,
functions: [],
entry: "__main",
@@ -445,7 +445,7 @@ test("semantic C cache refuses comment-only edits in multi-source graphs", async
const importedSource = "export function helper(): number { return 1; }\n";
await writeFile(imported, importedSource);
const semanticMod = {
irVersion: 12,
irVersion: 13,
sourceFile: f.source,
functions: [{
name: "__main",
@@ -66,7 +66,7 @@ function fixture(): IrModule {
);
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }],
@@ -136,7 +136,7 @@ function receiverReassignmentFixture(): IrModule {
];
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
functions: [{ name: "__main", params: [], returnType: VOID, locals, body, loc }],
@@ -201,7 +201,7 @@ function integerLoopFixture(mutatesIndex = false): IrModule {
{ kind: "bytesSet", arr: bytesRef(), index: indexRef(), value: ref("sum"), loc },
);
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
functions: [{
@@ -328,7 +328,7 @@ test("large record clones stay outlined while small clones remain inlineable", (
const fields = Array.from({ length: count }, (_, i) => ({ name: `f${i}`, type: F64 }));
const type = { kind: "record", shapeId: id } as const;
return {
irVersion: 12,
irVersion: 13,
sourceFile: "record-clone.ts",
entry: "__main",
records: [{ id, fields }],
+3 -3
View File
@@ -35,7 +35,7 @@ test("strings: literals, concat in a loop, toString, RC-clean under audit", asyn
// while (i < 3) { acc = acc + ("-" + i); i = i + 1; }
// console.log(acc, acc === "x-0-1-2", "α∂" < "β");
const mod: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "s.ts",
entry: "__main",
functions: [
@@ -113,7 +113,7 @@ test("short-circuit: right operand of && only evaluates when left is true", asyn
// if (false && sideEffect()) {} ; if (true || sideEffect()) {}
// console.log("done")
const mod: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "l.ts",
entry: "__main",
functions: [
@@ -161,7 +161,7 @@ test("string params: callee owns and releases; returns transfer ownership", asyn
// function greet(who: string): string { return "hi " + who; }
// console.log(greet("world"));
const mod: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "p.ts",
entry: "__main",
functions: [
+1 -1
View File
@@ -18,7 +18,7 @@ const n = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, lo
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
export const fibModule: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "fib.ts",
entry: "__main",
functions: [
@@ -8,7 +8,7 @@ test("scalar fromCharCode avoids a heap argument pack in both backends", () => {
const loc = { file: "scalar-char.ts", start: 0, end: 0 };
const value: IrExpr = { kind: "numLit", value: 65, type: F64, loc };
const mod: IrModule = {
irVersion: 12, sourceFile: loc.file, entry: "__main", globals: [],
irVersion: 13, sourceFile: loc.file, entry: "__main", globals: [],
functions: [{
name: "__main", params: [], returnType: VOID, locals: [], loc,
body: [{ kind: "exprStmt", loc, expr: {
+10 -4
View File
@@ -22,7 +22,7 @@ test("fib module JSON round-trips", () => {
test("validator rejects type mismatches and bad references", () => {
const loc = { file: "t.ts", start: 0, end: 0 };
const bad: IrModule = {
irVersion: 12,
irVersion: 13,
sourceFile: "t.ts",
entry: "__main",
functions: [
@@ -54,7 +54,7 @@ test("validator rejects type mismatches and bad references", () => {
]);
});
test("serializer round-trips ±Infinity and refuses NaN", () => {
test("serializer round-trips ±Infinity and NaN", () => {
const mod = structuredClone(fibModule);
const fn = mod.functions[0]!;
const stmt = fn.body[0]!;
@@ -79,11 +79,17 @@ test("serializer round-trips ±Infinity and refuses NaN", () => {
if (stmt.kind === "if" && stmt.cond.kind === "bin" && stmt.cond.right.kind === "numLit") {
stmt.cond.right.value = NaN;
}
expect(() => serializeModule(mod)).toThrow(/NaN/);
const back3 = deserializeModule(serializeModule(mod));
const stmt4 = back3.functions[0]!.body[0]!;
if (stmt4.kind === "if" && stmt4.cond.kind === "bin" && stmt4.cond.right.kind === "numLit") {
expect(stmt4.cond.right.value).toBeNaN();
} else {
throw new Error("round-trip lost the statement shape");
}
});
test("deserializer rejects the previous IR version", () => {
const json = serializeModule(fibModule).replace('"irVersion": 12', '"irVersion": 11');
const json = serializeModule(fibModule).replace('"irVersion": 13', '"irVersion": 12');
expect(() => deserializeModule(json)).toThrow(/version mismatch/);
});
@@ -19,7 +19,7 @@ const dyn = (): IrExpr => ({ kind: "dynFrom", value: str("value"), type: DYN, lo
function libCallModule(fn: IrLibFn, args: IrExpr[], type: IrType): IrModule {
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
functions: [{
@@ -58,7 +58,7 @@ function fixture(): IrModule {
{ kind: "varDecl", localId: "other", init: str("other"), loc },
);
return {
irVersion: 12,
irVersion: 13,
sourceFile: loc.file,
entry: "__main",
globals: [{ id: "%g.e.acc", name: "globalAccumulator", type: STRING, mutable: true }],
@@ -7595,6 +7595,18 @@
],
"diags": []
},
"<repo>/tests/corpus/3119-runtime-optional-spread.ts": {
"order": [
"<repo>/tests/corpus/3119-runtime-optional-spread.ts"
],
"diags": []
},
"<repo>/tests/corpus/3120-ir-nonfinite-numbers.ts": {
"order": [
"<repo>/tests/corpus/3120-ir-nonfinite-numbers.ts"
],
"diags": []
},
"<repo>/tests/corpus/400-fib.ts": {
"order": [
"<repo>/tests/corpus/400-fib.ts"
@@ -9796,6 +9808,12 @@
],
"diags": []
},
"<repo>/tests/diagnostics/spread-runtime-optional.ts": {
"order": [
"<repo>/tests/diagnostics/spread-runtime-optional.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/statements.ts": {
"order": [
"<repo>/tests/diagnostics/statements.ts"
@@ -0,0 +1,44 @@
interface Entry { kind: string; tag: number; values: string[] }
const source: Entry[] = [
{ kind: "first", tag: 1, values: ["one"] },
{ kind: "second", tag: 2, values: ["two"] },
];
const defaults: Entry = { kind: "absent", tag: -1, values: ["default"] };
// Even dense slice callbacks use the runtime-optional record ABI.
const dense = source.slice().map((entry) => ({ ...defaults, ...entry, label: "copy" }));
console.log("dense", dense.map((entry) => entry.kind).join(","));
const selected: (Entry | undefined)[] = source.slice();
// An explicit undefined slot is visited by map, unlike a hole.
selected.push(undefined);
const copied = selected.map((entry, index) => ({ ...defaults, ...entry, index }));
for (const entry of copied) {
console.log(entry.kind, entry.tag, entry.values.join(","), entry.index);
}
const firstCopy = copied[0];
if (firstCopy !== undefined) firstCopy.kind = "changed";
console.log("fresh", source[0]!.kind, copied[0]!.kind);
console.log("shared", copied[0]!.values === source[0]!.values);
console.log("default", copied[2]!.values === defaults.values);
copied[0]!.values.push("retained");
console.log("source", source[0]!.values.join(","));
// The lowered source can also be narrower than its checker union.
const narrowed = selected.filter((entry) => entry !== undefined).map((entry) => {
if (entry === undefined) return { ...defaults, label: "empty" };
return { ...defaults, ...entry, label: "present:" + entry.kind };
});
for (const entry of narrowed) console.log(entry.kind, entry.label);
// Later explicit fields replace source fields, including absent sources.
const overwritten = selected.map((entry, index) => ({
...entry, kind: "override", tag: index, values: ["new"],
}));
for (const entry of overwritten) console.log(entry.kind, entry.tag, entry.values.join(","));
// Declared optional destinations represent the no-copy arm directly.
interface Optional { kind?: string; tag?: number; values?: string[]; index: number }
const optional = selected.map((entry, index): Optional => ({ ...entry, index }));
for (const entry of optional) {
console.log(entry.kind ?? "missing", entry.tag ?? -2, entry.values?.join(",") ?? "missing", entry.index);
}
+15
View File
@@ -0,0 +1,15 @@
// These constants must also survive --emit=ir and a native emitter's input.
const numbers = [NaN, Number.NaN, Infinity, -Infinity, -0, 0, Number.MIN_VALUE, Number.MAX_VALUE];
for (const value of numbers) {
console.log(String(value), Number.isNaN(value), Number.isFinite(value), Object.is(value, -0));
}
function equal(left: number, right: number): boolean { return left === right; }
console.log("nan", equal(NaN, NaN), Object.is(NaN, NaN), [NaN].includes(NaN));
const values = new Map<number, string>();
values.set(NaN, "first");
values.set(Number.NaN, "second");
values.set(-0, "zero");
console.log("map", values.size, values.get(NaN), values.get(0));
const bytes = new Float64Array([NaN, -0, Infinity, -Infinity]);
console.log("bytes", Number.isNaN(bytes[0]), Object.is(bytes[1], -0), bytes[2], bytes[3]);
console.log("json", JSON.stringify({ numbers, nan: NaN, negativeZero: -0 }));
@@ -0,0 +1,5 @@
const values = [{ name: "first", id: 1 }].slice();
// The array callback ABI includes undefined; spreading that arm cannot
// initialize the required fields inferred by TypeScript.
const copies = values.map((value) => ({ ...value, label: "copy" }));
console.log(copies.length);
+40
View File
@@ -0,0 +1,40 @@
import { readFileSync, writeFileSync } from "node:fs";
import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js";
import { emitLlvmModule } from "../../../packages/compiler/src/backend/llvm/emitter.js";
import { LlvmUnsupportedError } from "../../../packages/compiler/src/backend/llvm/unsupported.js";
interface Request {
targetTriple?: string;
debug: boolean;
sources: { file: string; text: string }[];
pointerBits: 32 | 64;
wasi: boolean;
emitLibraryIdentity: boolean;
runtimeAbiMarker: boolean;
}
const module = deserializeModule(readFileSync(process.argv[2]!, "utf8"));
const request = JSON.parse(readFileSync(process.argv[4]!, "utf8")) as Request;
const sources = new Map<string, string>();
for (const source of request.sources) sources.set(source.file, source.text);
try {
const text = emitLlvmModule(module, {
...(request.targetTriple === undefined ? {} : { targetTriple: request.targetTriple }),
...(request.debug ? { debugSources: sources } : {}),
pointerBits: request.pointerBits,
wasi: request.wasi,
emitLibraryIdentity: request.emitLibraryIdentity,
runtimeAbiMarker: request.runtimeAbiMarker,
});
writeFileSync(process.argv[3]!, text);
} catch (error) {
if (error instanceof LlvmUnsupportedError) {
console.log("unsupported: " + error.message);
process.exitCode = 2;
} else if (error instanceof Error) {
console.log(error.name + ": " + error.message);
process.exitCode = 1;
} else {
throw error;
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ try {
console.log(JSON.stringify(errors));
process.exitCode = 1;
} else {
// Exercise the serializer's refusal through a real number literal,
// without placing an invalid non-JSON number in the input document.
// Introduce a native NaN before serialization, independently of the
// input document's sentinel decoding.
if (process.argv[3] === "nan") {
const statement = mod.functions[0]!.body[0];
if (statement?.kind === "exprStmt" && statement.expr.kind === "numLit") statement.expr.value = NaN;
@@ -0,0 +1,6 @@
spread-runtime-optional.ts:4:41 - error SC1090: object spread of '{ id: number; name: string } | undefined' sources where 'id' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides }) is not supported yet
3 | // initialize the required fields inferred by TypeScript.
4 | const copies = values.map((value) => ({ ...value, label: "copy" }));
| ^~~~~~~~
5 | console.log(copies.length);
@@ -0,0 +1,60 @@
import { deflateRawSync } from "node:zlib";
import { expect, test } from "vitest";
import { llvmBytes } from "../../packages/compiler/src/backend/literals.js";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js";
const item = cEmitterCases().find((item) => item.name === "level-nine module and facade compression")!;
const original = emitLlvmModule(item.module);
function withSource(bytes: Buffer): string {
return original
.replace(/@sc_npm_src_0 = internal constant \[\d+ x i8\] c"[^"\n]*"/, `@sc_npm_src_0 = internal constant [${bytes.length + 1} x i8] c"${llvmBytes(bytes)}"`)
.replace(/(ptr @sc_npm_src_0, i64 )\d+/, `$1${bytes.length}`);
}
test("LLVM comparison accepts different valid encodings of the same source", () => {
const source = item.module.embedded!.modules[0]!.source;
const stored = deflateRawSync(source, { level: 0 });
const compressed = deflateRawSync(source, { level: 9 });
expect(stored.equals(compressed)).toBe(false);
expect(normalizedEmbeddingLlvm(withSource(stored), item.module)).toBe(normalizedEmbeddingLlvm(withSource(compressed), item.module));
});
test("LLVM comparison rejects a corrupted DEFLATE stream", () => {
expect(() => normalizedEmbeddingLlvm(withSource(Buffer.from([255, 255, 255])), item.module)).toThrow();
});
test("LLVM comparison rejects a valid stream containing different source", () => {
const bytes = deflateRawSync("a different module");
expect(() => normalizedEmbeddingLlvm(withSource(bytes), item.module)).toThrow("embedded source differs");
});
test("LLVM comparison verifies declared array and encoded lengths", () => {
const array = original.replace(/(@sc_npm_src_0 = internal constant \[)\d+/, "$11");
expect(() => normalizedEmbeddingLlvm(array, item.module)).toThrow("incorrect array length");
const encoded = original.replace(/(ptr @sc_npm_src_0, i64 )\d+/, "$10");
expect(() => normalizedEmbeddingLlvm(encoded, item.module)).toThrow("incorrect encoded length");
});
test("LLVM comparison verifies inflated byte length and NUL termination", () => {
const raw = original.replace(/(ptr @sc_npm_src_0, i64 \d+, i64 )\d+/, "$11");
expect(() => normalizedEmbeddingLlvm(raw, item.module)).toThrow("incorrect raw length");
const terminator = original.replace(/(@sc_npm_src_0 = internal constant \[\d+ x i8\] c"[^"\n]*)\\00"/, "$1\\01\"");
expect(() => normalizedEmbeddingLlvm(terminator, item.module)).toThrow("NUL terminator");
});
test("LLVM comparison preserves edge conditions and runtime installation", () => {
const changed = original.replace("@scr_zlib_inflate_exact", "@wrong_inflater");
expect(normalizedEmbeddingLlvm(changed, item.module)).not.toBe(normalizedEmbeddingLlvm(original, item.module));
const changedEdge = original.replace(/(ptr @sc_npm_edge_0_to, i32 )\d+/, "$199");
expect(changedEdge).not.toBe(original);
expect(normalizedEmbeddingLlvm(changedEdge, item.module)).not.toBe(normalizedEmbeddingLlvm(original, item.module));
});
test("LLVM comparison checks ESM facades as well as source modules", () => {
const changed = structuredClone(item.module);
changed.embedded!.modules[0]!.esm = "export default 'wrong';";
expect(() => normalizedEmbeddingLlvm(original, changed)).toThrow("embedded source differs for sc_npm_esm_0");
});
@@ -0,0 +1,51 @@
import { inflateRawSync } from "node:zlib";
import type { IrModule } from "../../packages/compiler/src/ir/ir.js";
/** Parse exactly the LLVM byte-string grammar emitted by llvmBytes. */
function literalBytes(literal: string): Buffer {
const bytes: number[] = [];
for (let i = 0; i < literal.length; i++) {
const code = literal.charCodeAt(i);
if (code === 92) {
const escape = literal.slice(i + 1, i + 3);
if (!/^[0-9a-fA-F]{2}$/.test(escape)) throw new Error("invalid LLVM byte escape");
bytes.push(parseInt(escape, 16));
i += 2;
} else {
if (code < 32 || code > 126 || code === 34) throw new Error("invalid LLVM literal byte");
bytes.push(code);
}
}
if (bytes.pop() !== 0) throw new Error("LLVM literal lacks its NUL terminator");
return Buffer.from(bytes);
}
/** Different zlib implementations can produce different valid compressed
* streams. Check payload bytes, array bounds and both table lengths before
* canonicalizing only those encoding-dependent parts of the LLVM module. */
export function normalizedEmbeddingLlvm(text: string, mod: IrModule): string {
let normalized = text;
const check = (name: string, source: string): void => {
const declaration = new RegExp(`@${name} = internal constant \\[(\\d+) x i8\\] c"([^"\\n]*)"`);
const match = declaration.exec(normalized);
if (!match) throw new Error(`missing embedded declaration ${name}`);
const encoded = literalBytes(match[2]!);
if (Number(match[1]) !== encoded.length + 1) throw new Error(`incorrect array length for ${name}`);
const metadata = new RegExp(`ptr @${name}, (i32|i64) (\\d+), (i32|i64) (\\d+)`);
const lengths = metadata.exec(normalized);
if (!lengths) throw new Error(`missing length metadata for ${name}`);
if (lengths[1] !== lengths[3]) throw new Error(`inconsistent length types for ${name}`);
if (Number(lengths[2]) !== encoded.length) throw new Error(`incorrect encoded length for ${name}`);
const rawLength = Number(lengths[4]);
const decoded = rawLength === 0 ? encoded : inflateRawSync(encoded);
if (!decoded.equals(Buffer.from(source, "utf8"))) throw new Error(`embedded source differs for ${name}`);
if (rawLength !== 0 && rawLength !== decoded.length) throw new Error(`incorrect raw length for ${name}`);
normalized = normalized.replace(declaration, `@${name} = internal constant [decoded] c"${decoded.toString("base64")}"`);
normalized = normalized.replace(metadata, `ptr @${name}, ${lengths[1]} encoded, ${lengths[3]} ${rawLength}`);
};
for (const [index, module] of (mod.embedded?.modules ?? []).entries()) {
check(`sc_npm_src_${index}`, module.source);
if (module.esm !== undefined) check(`sc_npm_esm_${index}`, module.esm);
}
return normalized;
}
@@ -0,0 +1,99 @@
import { F64, type IrModule } from "../../packages/compiler/src/ir/ir.js";
import { numLit } from "../../packages/compiler/src/ir/build.js";
import type { LlvmTargetOptions } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
export interface LlvmEmitterRequest {
targetTriple?: string;
debug: boolean;
sources: { file: string; text: string }[];
pointerBits: 32 | 64;
wasi: boolean;
emitLibraryIdentity: boolean;
runtimeAbiMarker: boolean;
}
export function llvmEmitterRequest(options: Partial<LlvmEmitterRequest> = {}): LlvmEmitterRequest {
return { debug: false, sources: [], pointerBits: 64, wasi: false, emitLibraryIdentity: true, runtimeAbiMarker: false, ...options };
}
export function llvmEmitterOptions(request: LlvmEmitterRequest): LlvmTargetOptions {
return {
...(request.targetTriple === undefined ? {} : { targetTriple: request.targetTriple }),
...(request.debug ? { debugSources: new Map(request.sources.map((source) => [source.file, source.text])) } : {}),
pointerBits: request.pointerBits,
wasi: request.wasi,
emitLibraryIdentity: request.emitLibraryIdentity,
runtimeAbiMarker: request.runtimeAbiMarker,
};
}
export interface LlvmEmitterCase {
name: string;
module: IrModule;
request: LlvmEmitterRequest;
contains: string[];
excludes: string[];
}
export function llvmEmitterCases(): LlvmEmitterCase[] {
// Both complete backends consume the same IR, including library entry
// points, recursive layouts, closure boxes and embedded package tables.
const cases: LlvmEmitterCase[] = cEmitterCases().map((item) => ({
name: item.name, module: item.module,
request: llvmEmitterRequest({ debug: item.sources.length > 0, sources: item.sources }),
contains: item.module.lib ? ["@native_init", "@native_collect"] : ["define i32 @main("],
excludes: item.module.lib ? ["define i32 @main("] : [],
}));
const find = (name: string) => cases.find((item) => item.name === name)!;
find("source locations with multibyte text").contains.push("!DILocation", "!DICompileUnit");
find("global initialization and shutdown ownership").contains.push("@scr_str_release", "@scr_arr_release");
find("recursive record trace and teardown").contains.push("@scr_cyc_alloc", "@scr_cyc_free");
find("closure capture boxes").contains.push("@sc_retain_box", "@scr_closure_new");
find("thread-local library globals").contains.push("thread_local");
find("level-nine module and facade compression").contains.push("@scr_zlib_inflate_exact");
for (const name of ["empty executable", "scalar, reference and tuple layouts", "class forest and virtual dispatch tables", "union scalar payloads and immortal units"]) {
const base = find(name);
cases.push({
...base, name: `WASI 32-bit ${name}`,
request: llvmEmitterRequest({ pointerBits: 32, wasi: true }),
contains: ["%ScrStr = type { i32, i32, i32 }"],
excludes: ["%ScrStr = type { i64, i64, i64 }"],
});
}
const library = find("library entry points and identity constants");
cases.push({
...library, name: "library with external identity getters",
request: llvmEmitterRequest({ emitLibraryIdentity: false }),
contains: ["@native_init"], excludes: ["define ptr @native_build_id", "define i32 @native_abi"],
});
const empty = find("empty executable");
cases.push({
...empty, name: "runtime ABI marker reference", request: llvmEmitterRequest({ runtimeAbiMarker: true }),
contains: ["call void @scr_runtime_abi_"], excludes: [],
});
const ffi = structuredClone(empty.module);
const loc = ffi.functions[0]!.loc;
ffi.ffiImports = [{ name: "narrow", symbol: "native_narrow", params: ["i8", "u16", "f32"], returns: "i16" }];
ffi.functions[0]!.body = [{
kind: "exprStmt", loc,
expr: { kind: "ffiCall", import: "narrow", args: [-7, 65535, 0.5].map((value) => numLit(value, loc)), type: F64, loc },
}];
for (const [targetTriple, extend] of [
["arm64-apple-macos", true],
["x86_64-unknown-linux-gnu", true],
["aarch64-unknown-linux-gnu", false],
["x86_64-pc-windows-msvc", false],
["wasm32-unknown-wasi", true],
] as const) {
const declaration = extend
? "declare signext i16 @native_narrow(i8 signext, i16 zeroext, float)"
: "declare i16 @native_narrow(i8, i16, float)";
cases.push({
name: `narrow FFI ABI ${targetTriple}`, module: ffi,
request: llvmEmitterRequest({ targetTriple, pointerBits: targetTriple.startsWith("wasm32") ? 32 : 64 }),
contains: [declaration, "fptrunc double"], excludes: [],
});
}
return cases;
}
@@ -0,0 +1,182 @@
import { execFile, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile, compileC, deserializeModule, serializeModule, validateModule } from "@scriptc/compiler";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import {
moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect,
moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSymbol, moduleUsesZlib, type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
import { llvmEmitterCases, llvmEmitterOptions, llvmEmitterRequest, type LlvmEmitterRequest } from "./self-hosting-llvm-emitter-cases.js";
import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js";
const root = fileURLToPath(new URL("../..", import.meta.url));
const entry = join(root, "tests/fixtures/self-hosting/llvm-emitter.ts");
const execFileAsync = promisify(execFile);
const runOptions = { cwd: root, timeout: 60_000, maxBuffer: 256 * 1024 * 1024 };
const sanitize = process.env["SCRIPTC_SAN"] === "1";
function nativeFeatures(mod: IrModule) {
return {
regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod),
inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod),
symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), zlib: moduleUsesZlib(mod),
assert: moduleUsesAssert(mod),
textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
};
}
// Execute generated modules across expression dispatch, shared mutable
// emitter state, exception paths, dynamic walkers and callback adapters.
const programs = [
"001-hello.ts",
"101-arithmetic.ts",
"600-closures-basic.ts",
"711-inheritance-dispatch.ts",
"756-cycle-inheritance.ts",
"803-switch-rc-stress.ts",
"1005-json-nested.ts",
"1023-async-rc-stress.ts",
"1401-typedarray-slice-set.ts",
"1452-return-through-finally.ts",
"1637-inspect-dyn.ts",
"1672-symbol-containers.ts",
"1726-promise-with-resolvers.ts",
"2012-generators-return-throw.ts",
"2537-destructuring-assign-member-targets.ts",
"2840-finally-completions.ts",
"2905-bigint-edges.ts",
"3103-scalar-record-nested-control-flow.ts",
"3104-native-analysis-tables-loops.ts",
"3109-identity-union-collections.ts",
"3112-union-switch-control-flow.ts",
"3113-union-array-removal.ts",
"3114-union-record-field-write.ts",
"3115-zlib-static-compression-levels.ts",
"3116-sequence-expression-lifetimes.ts",
"3117-unknown-switch-control-flow.ts",
"3118-recursive-union-spread.ts",
"3119-runtime-optional-spread.ts",
"3120-ir-nonfinite-numbers.ts",
"text-codec-values/main.ts",
];
for (const backend of ["c", "llvm"] as const) {
test(`the complete LLVM emitter bootstraps natively (${backend})`, async () => {
const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-llvm-emitter-"));
const executable = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : ""));
const stage = executable("emitter");
const input = join(dir, "input.json");
const output = join(dir, "native.ll");
const config = join(dir, "config.json");
try {
// Lower the full production graph in a child so synchronous frontend
// work cannot block Vitest's worker RPC while it compiles the seed.
const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href;
const { stdout } = await execFileAsync(process.execPath, [
"--import", "tsx", "--input-type=module", "--eval",
`import { compile } from ${JSON.stringify(api)};
const result = await compile(process.argv[1], {
outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4],
dynamic: false, optimization: 'dev', sanitize: process.argv[5] === '1', emitIr: true,
});
console.log(JSON.stringify(result));`,
entry, dir, stage, backend, sanitize ? "1" : "0",
], { ...runOptions, timeout: 600_000 });
const built = JSON.parse(stdout) as Awaited<ReturnType<typeof compile>>;
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
if (!("binaryPath" in built)) throw new Error("bootstrap did not produce an executable");
expect(built.backend).toBe(backend);
expect(built.llvmRefusal).toBeUndefined();
expect(built.irPath).toBeDefined();
const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8"));
expect(ownIr.functions.length).toBeGreaterThan(800);
expect(validateModule(ownIr)).toEqual([]);
const emit = async (mod: IrModule, name: string, request = llvmEmitterRequest()): Promise<string> => {
writeFileSync(input, serializeModule(mod));
writeFileSync(config, JSON.stringify(request));
const result = await execFileAsync(stage, [input, output, config], { ...runOptions, timeout: 300_000 })
.catch((cause: unknown) => { throw new Error(`native LLVM emission failed for ${name}`, { cause }); });
expect(result.stdout, name).toBe("");
expect(result.stderr, name).toBe("");
const text = readFileSync(output, "utf8");
const expected = emitLlvmModule(mod, llvmEmitterOptions(request));
expect(normalizedEmbeddingLlvm(text, mod), name).toBe(normalizedEmbeddingLlvm(expected, mod));
return text;
};
for (const item of llvmEmitterCases()) {
expect(validateModule(item.module), item.name).toEqual([]);
const text = await emit(item.module, item.name, item.request);
for (const fragment of item.contains) expect(text, item.name).toContain(fragment);
for (const fragment of item.excludes) expect(text, item.name).not.toContain(fragment);
}
// A refusal must leave the requested output untouched, with the same
// exception category/message under Node and the compiled emitter.
const rejected = llvmEmitterCases()[0]!.module;
rejected.classes = [{ name: "UnimplementedNative", fields: [], runtime: true, loc: { file: rejected.sourceFile, start: 0, end: 0 } }];
writeFileSync(input, serializeModule(rejected));
writeFileSync(config, JSON.stringify(llvmEmitterRequest()));
writeFileSync(output, "untouched");
const args = [input, output, config];
const oracleRefusal = spawnSync(process.execPath, ["--import", "tsx", entry, ...args], runOptions);
const nativeRefusal = spawnSync(stage, args, runOptions);
for (const result of [oracleRefusal, nativeRefusal]) {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(2);
expect(result.stderr.toString()).toBe("");
}
expect(nativeRefusal.stdout).toEqual(oracleRefusal.stdout);
expect(nativeRefusal.stdout.toString()).toContain("classDef:UnimplementedNative");
expect(readFileSync(output, "utf8")).toBe("untouched");
for (const source of programs) {
const sourcePath = join(root, "tests/corpus", source);
const irPath = join(dir, "program.ir.json");
const lowered = await compile(sourcePath, { outDir: dir, outPath: irPath, outputKind: "ir", dynamic: false });
if (!lowered.ok) throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`);
const mod = deserializeModule(readFileSync(irPath, "utf8"));
const request: LlvmEmitterRequest = llvmEmitterRequest({
debug: true, sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
});
const text = await emit(mod, source, request);
const llvmPath = join(dir, "program.ll");
const outPath = executable("program");
writeFileSync(llvmPath, text);
await compileC({ cPath: llvmPath, outPath, sanitize, optimization: "dev", ...nativeFeatures(mod) });
const node = spawnSync(process.execPath, [sourcePath], runOptions);
const native = spawnSync(outPath, [], runOptions);
for (const result of [node, native]) {
expect(result.error, source).toBeUndefined();
expect(result.signal, `${source}: ${result.stderr}`).toBeNull();
expect(result.status, `${source}: ${result.stderr}`).toBe(0);
}
expect(native.stdout, source).toEqual(node.stdout);
expect(native.stderr, source).toEqual(node.stderr);
}
// The seed emits its own complete IR, including the production
// optimizer and LLVM expression/library graph. Compile those bytes
// and require a second native generation to reproduce the module.
const self = await emit(ownIr, "LLVM emitter emits itself");
const selfPath = join(dir, "self.ll");
const secondStage = executable("emitter-second");
writeFileSync(selfPath, self);
await compileC({ cPath: selfPath, outPath: secondStage, sanitize, optimization: "dev", ...nativeFeatures(ownIr) });
const again = join(dir, "again.ll");
const second = await execFileAsync(secondStage, [input, again, config], { ...runOptions, timeout: 300_000 });
expect(second.stdout).toBe("");
expect(second.stderr).toBe("");
expect(readFileSync(again, "utf8")).toBe(self);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, 1_200_000);
}
@@ -22,7 +22,7 @@ function numericModule(): IrModule {
functions: [{
name: "main", params: [], returnType: VOID, locals: [], loc,
body: [
// The fixture's failure mode changes this first literal to NaN.
// The fixture can change this first literal to NaN before encoding.
{ kind: "exprStmt", expr: numLit(0, loc), loc },
{ kind: "exprStmt", expr: { kind: "intrinsic", name: "console.log", args: numbers.map((n) => numLit(n, loc)), type: VOID, loc }, loc },
{ kind: "exprStmt", expr: { kind: "intrinsic", name: "console.log", args: [{
@@ -126,9 +126,13 @@ for (const backend of ["c", "llvm"] as const) {
expect(mismatch.native.stdout).toEqual(mismatch.oracle.stdout);
expect(mismatch.native.stdout.toString()).toContain("IR version mismatch");
const nan = run(serializeModule(numericModule()), ["nan"]);
expect(nan.native.status).toBe(1);
expect(nan.native.stdout).toEqual(nan.oracle.stdout);
expect(nan.native.stdout.toString()).toContain("IR contains NaN; refusing to serialize");
expect(nan.native.status).toBe(0);
expect(deserializeModule(nan.native.stdout.toString())).toEqual(deserializeModule(nan.oracle.stdout.toString()));
expect(nan.native.stdout.toString()).toContain('"$nonfinite": "nan"');
const fromNan = run(nan.native.stdout.toString());
expect(fromNan.native.status).toBe(0);
expect(fromNan.native.stdout).toEqual(nan.native.stdout);
expect(fromNan.native.stdout).toEqual(fromNan.oracle.stdout);
} finally {
rmSync(dir, { recursive: true, force: true });
}
@@ -4,7 +4,7 @@ import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/
const loc = { file: "validator-input.ts", start: 12, end: 34 };
const expression = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
const base = (): IrModule => ({
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
});