mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
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:
@@ -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 })) });
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 }],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user