diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de501f75..5b2522d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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) diff --git a/packages/compiler/src/backend/c/may-throw.test.ts b/packages/compiler/src/backend/c/may-throw.test.ts index 7d60cbe4..ea606008 100644 --- a/packages/compiler/src/backend/c/may-throw.test.ts +++ b/packages/compiler/src/backend/c/may-throw.test.ts @@ -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 }; diff --git a/packages/compiler/src/backend/cycle-analysis.test.ts b/packages/compiler/src/backend/cycle-analysis.test.ts index 32409a56..eb8a49d7 100644 --- a/packages/compiler/src/backend/cycle-analysis.test.ts +++ b/packages/compiler/src/backend/cycle-analysis.test.ts @@ -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 })) }); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 0120bd61..31c08dba 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 02f30bac..60e3d6e9 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -118,7 +118,7 @@ import { classStructSym, type LlClassMeta, } from "./classes.js"; -import { LlDyn } from "./dyn.js"; +import { LlDyn, type DynHost } from "./dyn.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { LlWalkers } from "./walkers.js"; import { @@ -134,7 +134,7 @@ import { traceArg, vAdapters, } from "./shapes.js"; -import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlStreamTypedRefContext, LlValue, LlvmEmitterContext } from "./expr-context.js"; +import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlStreamTypedRefContext, LlValue } from "./expr-context.js"; export { LlvmUnsupportedError } from "./unsupported.js"; @@ -202,13 +202,13 @@ function llStrBytes(text: string): string { return llBytes(Buffer.from(text, "utf8")); } -class LlEmitter { +export class LlEmitter { private readonly debug: LlvmDebugInfo | null; private debugScope: string | null = null; readonly sizeType: "i32" | "i64"; readonly ffiExtendNarrowIntegers: boolean; readonly cycleColorOffset: number; - private readonly wasi: boolean; + readonly wasi: boolean; private readonly emitLibraryIdentity: boolean; private readonly runtimeAbiMarker: boolean; /** Interned string literals: UTF-8 text → { symbol, byte length } — @@ -223,35 +223,37 @@ class LlEmitter { * (pattern, flags) pair; the bytecode slot starts null and the runtime * compiles it lazily on first use. The source/flags strings intern at * REGISTRATION (bodies emit before the literal table flushes). */ - private readonly regexInstances = new Map(); + readonly regexInstances = new Map(); /** Interned tagged-template strings objects: per-site key → { symbol, * interned cooked-literal refs }. One immortal ScrArr of string slots * per template SITE (the spec's per-occurrence identity — the C * emitter's templateStringsInstances discipline). */ - private readonly templateStringsInstances = new Map(); + readonly templateStringsInstances = new Map(); /** Interned NUL-terminated C-string constants (scr_jb_puts labels, the * stringify indent text): UTF-8 text → { symbol, byte length }. */ private readonly cstrs = new Map(); /** Type-directed walker functions (JSON serializers, the indent * rewriter, union ToString/join) — interned per typeKey/unionId, defs * flushed with the shape helpers. */ - private readonly walkers = new LlWalkers(this); + readonly walkers: LlWalkers; /** The dyn (ScrDyn dyn) helper registry — dyn.ts's interned ports of * walkers.ts's dyn slice. */ - private readonly dyn = new LlDyn(this); + readonly dyn: LlDyn; + /** Explicit callback boundary for standalone layout and walker stages. */ + readonly shapeHost: DynHost; /** External declarations, in first-use order. */ private readonly decls = new Set(); /** Declared functions referenced as values: each needs an env-signature * wrapper + an interned immortal closure (so `f === f` holds). */ - private readonly fnValues = new Set(); + readonly fnValues = new Set(); private needsOom = false; - private needsBadTag = false; - private needsBadKey = false; + needsBadTag = false; + needsBadKey = false; private needsRetainBox = false; - private readonly fnByName = new Map(); + readonly fnByName = new Map(); /** Manifest-bound native imports, used by ffiCall emission. */ - private readonly ffiByName = new Map(); + readonly ffiByName = new Map(); /** C-ABI callback trampolines and (for raw/no-userdata callbacks) their * distinct call-scoped TLS closure slots. */ private readonly ffiCallbackAdapters: Map; @@ -259,37 +261,37 @@ class LlEmitter { * descriptor anywhere in the manifest, every native call is a * pending-exception checkpoint (may-throw derives the same fact from * the same helper). */ - private readonly ffiHasRetainedCallback: boolean; + readonly ffiHasRetainedCallback: boolean; private readonly ffiHasForeignCallback: boolean; private readonly globalTypes = new Map(); private readonly tdzGlobals = new Map(); - private readonly constantNumericTables: ReadonlyMap; + readonly constantNumericTables: ReadonlyMap; /** May-throw analysis (the C emitter's computeMayThrow, shared): pending * checks are emitted only after calls that can actually raise. */ - private readonly mayThrow: Set; - private readonly indirectMayThrow: boolean; + readonly mayThrow: Set; + readonly indirectMayThrow: boolean; /** Method names with at least one may-throw implementation — the * virtualCall pending check's key (CEmitter.mayThrowMethods). */ - private readonly mayThrowMethods = new Set(); + readonly mayThrowMethods = new Set(); /** setTimeout and friends appeared somewhere: main must run the event * loop even in programs with no async functions (CEmitter.usesTimers). */ - private usesTimers = false; + usesTimers = false; /** Emitted ref-kind resolve thunks for new Promise, interned per inner * typeKey → thunk symbol (CEmitter.resolveThunks). */ - private readonly resolveThunks = new Map(); - private readonly resolveThunkDefs: string[] = []; + readonly resolveThunks = new Map(); + readonly resolveThunkDefs: string[] = []; /** ReadableStream.from adapters keep typed arrays by reference and box * one current element per pull. */ - private readonly streamFromArrayAdapters = new Map(); + readonly streamFromArrayAdapters = new Map(); /** Identity-preserving static→dyn capsules for program classes and Web * APIs whose values remain directly observable. */ - private readonly liveDynRefAdapters = new Map< + readonly liveDynRefAdapters = new Map< string, LlStreamTypedRefAdapter >(); /** Runtime-arm dispatchers for live union values. */ - private readonly liveDynUnionRefAdapters = new Map(); - private readonly dynPromiseAdapters = new Map(); + readonly liveDynUnionRefAdapters = new Map(); + readonly dynPromiseAdapters = new Map(); readonly unionsById = new Map(); readonly recordsById = new Map(); readonly recordCloneShapes = new Set(); @@ -297,7 +299,7 @@ class LlEmitter { readonly tracedUnions: Set; /** The class graph (buildClassGraph): preorder numbering, hierarchy * membership, virtual slot lists — the CEmitter classMeta, ported. */ - private readonly classMeta: Map; + readonly classMeta: Map; /** Class objects (classes as first-class values): className → the * interned .name literal ref — registered during body emission, the * statics and construct thunks assemble around the bodies. */ @@ -317,8 +319,8 @@ class LlEmitter { private readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = []; // ── per-function state (reset in emitFunction) ───────────────────────── - private B = new BlockBuilder(); - private frames: LlValue[][] = []; + B = new BlockBuilder(); + frames: LlValue[][] = []; private scopes: LlScopeEntry[][] = []; /** Enclosing break/continue targets. `kind` separates loops from * switches and labeled blocks: an unlabeled break binds to the innermost @@ -331,7 +333,7 @@ class LlEmitter { kind: "loop" | "block" | "switch"; brkLabel: string; contLabel: string | null; - labels?: string[]; + labels?: string[] | undefined; frameDepth: number; scopeDepth: number; finallyDepth: number; @@ -339,8 +341,8 @@ class LlEmitter { private currentLocals = new Map(); private captureIds = new Set(); /** Active canonical byte-loop induction bindings: local id → size_t slot. */ - private integerLoopBindings = new Map(); - private integerRanges: IntegerRanges = new Map(); + integerLoopBindings = new Map(); + integerRanges: IntegerRanges = new Map(); /** Enclosing try-with-FINALLY regions, innermost last: a `return` * inside one runs every crossed finally (innermost first) before the * actual ret — the C emitter's pending-return path, with the finally @@ -362,7 +364,7 @@ class LlEmitter { private currentReturnType: IrType = VOID; /** Active only while emitting a wasm32 async body lowered with LLVM's * switched-coroutine intrinsics. */ - private currentWasiCoro: { + currentWasiCoro: { kind: "async" | "generator"; id: string; handle: string; @@ -374,12 +376,12 @@ class LlEmitter { /** The generator channels of the function being emitted (null outside * generator bodies): yieldExpr emission reads them, and emitTryCatch's * catch prologue emits the GENRET sentinel re-unwind exactly here. */ - private currentGenerator: { yieldT: IrType; nextT: IrType } | null = null; + currentGenerator: { yieldT: IrType; nextT: IrType } | null = null; /** Active optional-chain bind slots, by chain id (chainRecv reads). */ - private readonly chainSlots = new Map(); - private logArgSlots = 0; + readonly chainSlots = new Map(); + logArgSlots = 0; - constructor(private readonly mod: IrModule, options: LlvmTargetOptions) { + constructor(readonly mod: IrModule, options: LlvmTargetOptions) { this.debug = options.debugSources === undefined ? null : new LlvmDebugInfo(mod.sourceFile, options.debugSources, options.pointerBits, mod.unions); this.constantNumericTables = findConstantNumericTables(mod); this.sizeType = options.pointerBits === 32 ? "i32" : "i64"; @@ -411,6 +413,24 @@ class LlEmitter { const traced = computeTraced(mod); this.tracedShapes = traced.shapes; this.tracedUnions = traced.unions; + this.shapeHost = { + sizeType: this.sizeType, + cycleColorOffset: this.cycleColorOffset, + tracedShapes: this.tracedShapes, + tracedUnions: this.tracedUnions, + recordsById: this.recordsById, + recordCloneShapes: this.recordCloneShapes, + unionsById: this.unionsById, + declare: (decl) => this.declare(decl), + needOom: () => this.needOom(), + needBadTag: () => this.needBadTag(), + internLiteral: (text) => this.internLiteral(text), + cstr: (text) => this.cstr(text), + unitInstanceRef: (unionId, tag) => this.unitInstanceRef(unionId, tag), + liveDynRefAdapter: (type) => this.liveDynRefAdapter(type), + }; + this.walkers = new LlWalkers(this.shapeHost); + this.dyn = new LlDyn(this.shapeHost); for (const g of mod.globals ?? []) { // Module globals: scalar (f64/bool) storage is a zero-initialized // LLVM global, ref-kind storage a null-initialized ptr — load/store @@ -466,13 +486,13 @@ class LlEmitter { } } - private abiOffset(native64: number, wasm32: number): number { + abiOffset(native64: number, wasm32: number): number { return this.sizeType === "i32" ? wasm32 : native64; } // ── types ─────────────────────────────────────────────────────────────── - private llType(t: IrType): string { + llType(t: IrType): string { if (POINTER_KINDS.has(t.kind) && t.kind !== "http2Session" && t.kind !== "http2Stream") return "ptr"; switch (t.kind) { case "f64": @@ -493,7 +513,7 @@ class LlEmitter { // ── module assembly ───────────────────────────────────────────────────── - private ffiCallbackAdapter(binding: string, id: string): FfiCallbackAdapter { + ffiCallbackAdapter(binding: string, id: string): FfiCallbackAdapter { const adapter = this.ffiCallbackAdapters.get(`${binding}:${id}`); if (!adapter) throw new InternalCompilerError(`llvm emitter bug: no callback adapter for ${binding}:${id}`); return adapter; @@ -802,7 +822,7 @@ class LlEmitter { // emit), then the file assembles around them — the C emitter's order. const fnDefs: string[] = []; for (const fn of this.mod.functions) fnDefs.push(this.emitFunction(fn)); - const layouts = emitLlvmLayouts(this, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t)); + const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t)); const shapes = layouts.records; const classShapes = layouts.classes; const classObjDefs = layouts.classObjects; @@ -858,7 +878,7 @@ class LlEmitter { if (!isRefCounted(g.type)) return; lines.push( ` %${prefix}${i} = load ptr, ptr @${mangleGlobal(g.id)}`, - ` call void ${releaseSym(this, g.type)}(ptr %${prefix}${i}) ; ${g.name}`, + ` call void ${releaseSym(this.shapeHost, g.type)}(ptr %${prefix}${i}) ; ${g.name}`, ); }); fnValueProps.forEach((name, i) => { @@ -1264,7 +1284,8 @@ class LlEmitter { `@sc_npm_modules = internal constant [${moduleRows.length} x %ScrIslandModule] [ ${moduleRows.join(", ")} ]`, ); embedded.edges.forEach((edge, i) => { - for (const [part, text] of [["from", edge.from], ["spec", edge.specifier], ["to", edge.to]] as const) { + const parts: [string, string][] = [["from", edge.from], ["spec", edge.specifier], ["to", edge.to]]; + for (const [part, text] of parts) { const bytes = Buffer.from(text, "utf8"); out.push(`@sc_npm_edge_${i}_${part} = internal constant [${bytes.length + 1} x i8] c"${llBytes(bytes)}"`); } @@ -1306,7 +1327,8 @@ class LlEmitter { // No event loop yet: timers/async still refuse (phase 5). const stamps: string[] = []; for (const iv of this.errorIntervals) { - for (const [field, value] of [[0, iv.pre], [1, iv.post]] as const) { + const fields: [number, number][] = [[0, iv.pre], [1, iv.post]]; + for (const [field, value] of fields) { stamps.push( ` store ${this.sizeType} ${value}, ptr getelementptr inbounds ([${RUNTIME_ERROR_CLASSES.size} x %ScrVt], ptr @scr_error_vts, i64 0, i64 ${iv.kind}, i32 ${field})${field === 1 ? ` ; ${iv.lib}` : ""}`, ); @@ -2004,17 +2026,17 @@ class LlEmitter { tr.push(` call void @scr_promise_fulfill_str(ptr %pr, ptr %r) ; moves in`); break; default: { - const v = vAdapters(this, ret); + const v = vAdapters(this.shapeHost, ret); this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); tr.push( - ` call void @scr_promise_fulfill_ref(ptr %pr, ptr %r, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(this, ret)})`, + ` call void @scr_promise_fulfill_ref(ptr %pr, ptr %r, ptr ${v.retain}, ptr ${v.release}, ptr ${traceArg(this.shapeHost, ret)})`, ); } } tr.push(` ret void`, `thrown:`); if (ret.kind !== "void" && isRefCounted(ret)) { // An escaping throw means %r is the never-read dummy (NULL). - tr.push(` call void ${releaseSym(this, ret)}(ptr %r)`); + tr.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r)`); } tr.push(` ret void`, `}`, ``); } @@ -2151,7 +2173,7 @@ class LlEmitter { tr.push(` call void @scr_gen_out_bool(ptr %g, i1 %r)`, ` br label %done`); break; default: { - const v = vAdapters(this, ret); + const v = vAdapters(this.shapeHost, ret); this.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); tr.push(` call void @scr_gen_out_ref(ptr %g, ptr %r, ptr ${v.release})`, ` br label %done`); } @@ -2167,7 +2189,7 @@ class LlEmitter { `dropdummy:`, ); if (ret.kind !== "void" && isRefCounted(ret)) { - tr.push(` call void ${releaseSym(this, ret)}(ptr %r) ; unwound: the never-read dummy`); + tr.push(` call void ${releaseSym(this.shapeHost, ret)}(ptr %r) ; unwound: the never-read dummy`); } tr.push(` br label %done`, `done:`, ` ret void`, `}`, ``); } @@ -2189,12 +2211,12 @@ class LlEmitter { const settleKey = `ags:${typeKey(genT)}`; if (!this.resolveThunks.has(settleKey)) { this.resolveThunks.set(settleKey, settleAsync); - const adapters = vAdapters(this, resultT); + const adapters = vAdapters(this.shapeHost, resultT); this.resolveThunkDefs.push( `define internal void @${settleAsync}(ptr %g, ptr %p) ${FN_ATTRS} {`, `entry:`, ` %r = call ptr @${build}(ptr %g)`, - ` call void @scr_promise_fulfill_ref(ptr %p, ptr %r, ptr ${adapters.retain}, ptr ${adapters.release}, ptr ${traceArg(this, resultT)})`, + ` call void @scr_promise_fulfill_ref(ptr %p, ptr %r, ptr ${adapters.retain}, ptr ${adapters.release}, ptr ${traceArg(this.shapeHost, resultT)})`, ` ret void`, `}`, ``, @@ -2220,7 +2242,7 @@ class LlEmitter { this.declare(`declare void @scr_closure_release(ptr)`); dr.push(` call void @scr_closure_release(ptr %dv${i})`); } else { - dr.push(` call void ${releaseSym(this, pType)}(ptr %dv${i})`); + dr.push(` call void ${releaseSym(this.shapeHost, pType)}(ptr %dv${i})`); } }); dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``); @@ -2308,19 +2330,19 @@ class LlEmitter { } /** Registers an owned refcounted value on the current statement frame. */ - private own(v: LlValue): LlValue { + own(v: LlValue): LlValue { if (isRefCounted(v.type)) this.currentFrame().push(v); return v; } /** Registers a SLOT whose current contents the frame owns (conditional * results: optional chains, branch joins that park ownership). */ - private ownSlot(slot: string, type: IrType): void { + ownSlot(slot: string, type: IrType): void { if (isRefCounted(type)) this.currentFrame().push({ name: slot, type, slot: true }); } /** Strike a refcounted temp from its frame: ownership is being moved. */ - private moveTemp(v: LlValue): void { + moveTemp(v: LlValue): void { if (!isRefCounted(v.type)) return; for (let i = this.frames.length - 1; i >= 0; i--) { const idx = this.frames[i]!.findIndex((e) => e.name === v.name); @@ -2334,19 +2356,19 @@ class LlEmitter { /** The retained (+1) read of a refcounted value — type-directed through * the `_v` table (immortals skip, exactly the C retain calls). */ - private retainValue(name: string, type: IrType): string { + retainValue(name: string, type: IrType): string { const t = this.B.tmp(); - this.B.line(`${t} = call ptr ${retainSym(this, type)}(ptr ${name})`); + this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`); return t; } /** The release call for one owned refcounted value — type-directed like * releaseCallC (all runtime releases are NULL-tolerant). */ - private releaseValue(name: string, type: IrType): void { - this.B.line(`call void ${releaseSym(this, type)}(ptr ${name})`); + releaseValue(name: string, type: IrType): void { + this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`); } - private releaseFrame(frame: LlValue[]): void { + releaseFrame(frame: LlValue[]): void { for (const v of frame) { if (v.slot) { const t = this.B.tmp(); @@ -2443,7 +2465,7 @@ class LlEmitter { * (per the may-throw analysis), test the pending flag and unwind. The * call's result temp must join its frame BEFORE this runs so the unwind * releases the dummy (NULL for refcounted kinds) harmlessly. */ - private emitPendingCheck(): void { + emitPendingCheck(): void { const B = this.B; if (B.isTerminated()) return; this.declare(`declare zeroext i1 @scr_exc_pending()`); @@ -2461,7 +2483,7 @@ class LlEmitter { * the `throw` statement's kind dispatch (stmts.ts's), shared with * every synthetic thrower. Ownership of a refcounted payload must have * been moved off its frame by the caller. */ - private emitThrowValue(v: LlValue): void { + emitThrowValue(v: LlValue): void { const B = this.B; const t = v.type; if (t.kind === "date") { @@ -2479,25 +2501,25 @@ class LlEmitter { // Hierarchy instances carry a vtable word: the OBJ kind keeps the // dynamic class inspectable (catch-binding instanceof, the uncaught // printer's "name: message" for Error instances). - const rc = vAdapters(this, t); + const rc = vAdapters(this.shapeHost, t); this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`); - B.line(`call void @scr_throw_obj(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, t)})`); + B.line(`call void @scr_throw_obj(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, t)})`); } else if (t.kind === "symbol" || t.kind === "bigint" || t.kind === "func" || t.kind === "classval") { - const rc = vAdapters(this, t); + const rc = vAdapters(this.shapeHost, t); this.declare(`declare void @scr_throw_primitive_ref(ptr, ptr, ptr, ptr)`); B.line(`call void @scr_throw_primitive_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr null)`); } else if (t.kind === "dyn" || t.kind === "jsval") { - const rc = vAdapters(this, t); + const rc = vAdapters(this.shapeHost, t); const test = t.kind === "dyn" ? "scr_dyn_is_object" : "scr_jsval_is_object"; this.declare(`declare zeroext i1 @${test}(ptr)`); this.declare(`declare void @scr_throw_ref_classified(ptr, ptr, ptr, ptr, i1 zeroext)`); const object = B.tmp(); B.line(`${object} = call zeroext i1 @${test}(ptr ${v.name})`); - B.line(`call void @scr_throw_ref_classified(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, t)}, i1 zeroext ${object})`); + B.line(`call void @scr_throw_ref_classified(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, t)}, i1 zeroext ${object})`); } else { - const rc = vAdapters(this, t); + const rc = vAdapters(this.shapeHost, t); this.declare(`declare void @scr_throw_ref(ptr, ptr, ptr, ptr)`); - B.line(`call void @scr_throw_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, t)})`); + B.line(`call void @scr_throw_ref(ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, t)})`); } } @@ -2505,7 +2527,7 @@ class LlEmitter { * union arms): one fcmp for f64 (NaN and both zeros compare !one), a * length load for strings, an inline tag switch for unions, `!= null` * for the always-truthy object kinds (JS: [] and {} are truthy). */ - private truthy(v: LlValue): string { + truthy(v: LlValue): string { const B = this.B; if (v.type.kind === "union") { // The ARM value's ToBoolean: an inline tag switch (the C emitter's @@ -2611,7 +2633,7 @@ class LlEmitter { // ── union plumbing ────────────────────────────────────────────────────── /** Loads a union box's tag (i32). */ - private unionTag(uName: string): string { + unionTag(uName: string): string { const p = this.B.tmp(); const t = this.B.tmp(); this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 1`); @@ -2621,7 +2643,7 @@ class LlEmitter { /** The BORROWED payload pointer of a ref arm (scr_union_peek inlined — * the runtime's is a static inline). */ - private unionPeek(uName: string): string { + unionPeek(uName: string): string { const p = this.B.tmp(); const t = this.B.tmp(); this.B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${uName}, i64 0, i32 5`); @@ -2632,7 +2654,7 @@ class LlEmitter { /** Emits `switch` over a union's tag with one block per arm; each arm * body must TERMINATE its block (the callers branch to a join). The * default block is the C emitter's invalid-tag abort. */ - private unionTagSwitch(uName: string, def: IrUnionDef, arm: (armType: IrType, tag: number) => void): void { + unionTagSwitch(uName: string, def: IrUnionDef, arm: (armType: IrType, tag: number) => void): void { const B = this.B; const tag = this.unionTag(uName); const bad = B.newLabel("u.bad"); @@ -2653,7 +2675,7 @@ class LlEmitter { /** The +1 extraction of a union's single narrowed arm (unionNarrow / * the nullish-family reads): scalars via the runtime getters, ref arms * a retained peek. */ - private unionExtract(uName: string, arm: IrType): string { + unionExtract(uName: string, arm: IrType): string { const B = this.B; if (arm.kind === "f64") { const t = B.tmp(); @@ -2673,7 +2695,7 @@ class LlEmitter { /** Constructs a union box around an OWNED (+1, already moved) value — * the scr_union_new_* dispatch of unionWrap and the wrap-into-join * sites (shift). */ - private unionNewOwned(tag: number, v: LlValue): string { + unionNewOwned(tag: number, v: LlValue): string { const B = this.B; const t = B.tmp(); if (v.type.kind === "f64") { @@ -2686,17 +2708,17 @@ class LlEmitter { B.line(`${t} = call ptr @scr_union_new_bool(i32 ${tag}, i1 ${v.name})`); return t; } - const rc = vAdapters(this, v.type); + const rc = vAdapters(this.shapeHost, v.type); this.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`); B.line( - `${t} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, v.type)})`, + `${t} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, v.type)})`, ); return t; } // ── class plumbing ────────────────────────────────────────────────────── - private classMetaOf(className: string): LlClassMeta { + classMetaOf(className: string): LlClassMeta { const meta = this.classMeta.get(className); if (!meta) throw new InternalCompilerError(`llvm emitter bug: unknown class ${className}`); return meta; @@ -2706,7 +2728,7 @@ class LlEmitter { * 1 on hierarchy members, then the flattened field list. Runtime error * classes GEP through %ScrError (their structs live in the runtime; the * def's [name, message, %code, %cause] order matches the layout). */ - private classFieldPtr(objName: string, className: string, field: string): { ptr: string; type: IrType } { + classFieldPtr(objName: string, className: string, field: string): { ptr: string; type: IrType } { const meta = this.classMetaOf(className); const { index, type } = classFieldIndex(meta, field); const p = this.B.tmp(); @@ -2718,7 +2740,7 @@ class LlEmitter { /** `o->vt->pre` — the dynamic class's preorder number (borrowed object; * the static class names which struct spelling carries the vt word). */ - private loadVtPre(objName: string, staticClassName: string): string { + loadVtPre(objName: string, staticClassName: string): string { const B = this.B; const vtp = B.tmp(); const vt = B.tmp(); @@ -2734,7 +2756,7 @@ class LlEmitter { /** The class object's static symbol (classes as values), registering it * for assembly on first use and interning the .name literal while the * table is still open (the regex-literal discipline). */ - private classObjSym(className: string): string { + classObjSym(className: string): string { if (!this.classObjs.has(className)) { const meta = this.classMetaOf(className); this.classObjs.set(className, { nameSym: this.internLiteral(meta.def.jsName ?? "") }); @@ -2744,14 +2766,14 @@ class LlEmitter { // ── record plumbing ───────────────────────────────────────────────────── - private recordShape(shapeId: string): IrRecordShape { + recordShape(shapeId: string): IrRecordShape { const shape = this.recordsById.get(shapeId); if (!shape) throw new InternalCompilerError(`llvm emitter bug: unknown record shape ${shapeId}`); return shape; } /** The field-slot pointer of a record member (rc header at index 0). */ - private recordFieldPtr(objName: string, shapeId: string, field: string): { ptr: string; type: IrType } { + recordFieldPtr(objName: string, shapeId: string, field: string): { ptr: string; type: IrType } { const shape = this.recordShape(shapeId); const idx = shape.fields.findIndex((f) => f.name === field); if (idx < 0) throw new InternalCompilerError(`llvm emitter bug: unknown field ${field} on shape ${shapeId}`); @@ -2763,7 +2785,7 @@ class LlEmitter { } /** The overflow map's slot pointer on an index-signature shape. */ - private recordOvfPtr(objName: string, shapeId: string): string { + recordOvfPtr(objName: string, shapeId: string): string { const shape = this.recordShape(shapeId); if (!shape.indexValue) throw new InternalCompilerError(`llvm emitter bug: shape ${shapeId} has no overflow map`); const p = this.B.tmp(); @@ -2776,7 +2798,7 @@ class LlEmitter { } /** Loads a record field (i8-stored bools trunc to i1). */ - private loadField(ptr: string, t: IrType): string { + loadField(ptr: string, t: IrType): string { const B = this.B; const fieldTy = llFieldType(t); const raw = B.tmp(); @@ -2788,7 +2810,7 @@ class LlEmitter { } /** Stores a record field (i1 zext to the i8 storage). */ - private storeField(ptr: string, t: IrType, value: string): void { + storeField(ptr: string, t: IrType, value: string): void { const B = this.B; const fieldTy = llFieldType(t); if (fieldTy !== "i8") { @@ -2804,7 +2826,7 @@ class LlEmitter { /** A binding's storage: a module global, a plain local slot, or a boxed * local (the slot holds the capture BOX; access goes through it). */ - private binding(id: string): { kind: "global" | "local" | "boxed"; slot: string; type: IrType; local?: IrLocal } { + binding(id: string): { kind: "global" | "local" | "boxed"; slot: string; type: IrType; local?: IrLocal } { const local = this.currentLocals.get(id); if (local) { return { @@ -2827,13 +2849,13 @@ class LlEmitter { } /** Loads a boxed binding's box pointer out of its slot. */ - private loadBox(slot: string): string { + loadBox(slot: string): string { const b = this.B.tmp(); this.B.line(`${b} = load ptr, ptr ${slot}`); return b; } - private boxGet(box: string, t: IrType): string { + boxGet(box: string, t: IrType): string { const B = this.B; const acc = boxAccess(t); const r = B.tmp(); @@ -2851,7 +2873,7 @@ class LlEmitter { } /** scr_box_set_* — the ref form takes ownership of the passed value. */ - private boxSet(box: string, t: IrType, value: string): void { + boxSet(box: string, t: IrType, value: string): void { const B = this.B; const acc = boxAccess(t); if (acc === "f64") { @@ -2866,7 +2888,7 @@ class LlEmitter { } } - private retainBox(box: string): string { + retainBox(box: string): string { this.needsRetainBox = true; const t = this.B.tmp(); this.B.line(`${t} = call ptr @sc_retain_box(ptr ${box})`); @@ -2880,7 +2902,7 @@ class LlEmitter { const B = this.B; const acc = boxAccess(t); const cell = B.tmp(); - B.line(`${cell} = ${arrNewCall(this, t, "1")} ; TDZ cell`); + B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`); this.arrPush(cell, acc === "bool" ? "bool" : "f64", value); this.declare(`declare void @scr_box_set_ref(ptr, ptr)`); B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`); @@ -2902,7 +2924,7 @@ class LlEmitter { return slotv; } - private checkGlobalTdz(id: string): void { + checkGlobalTdz(id: string): void { const name = this.tdzGlobals.get(id); if (name === undefined) return; const B = this.B; @@ -2929,7 +2951,7 @@ class LlEmitter { B.startBlock(lk); } - private tdzBoxRead(box: string, t: IrType, name: string): string { + tdzBoxRead(box: string, t: IrType, name: string): string { const slotv = this.checkTdz(box, name); const B = this.B; const acc = boxAccess(t); @@ -2947,7 +2969,7 @@ class LlEmitter { /** Declaration stores initialize an empty TDZ box. Later stores check * it after the RHS and update scalar cells in place. References move in. */ - private writeBindingBox(box: string, local: IrLocal, value: string, initializes = false, borrowed = false): void { + writeBindingBox(box: string, local: IrLocal, value: string, initializes = false, borrowed = false): void { if (local.tdz) { const first = initializes || !local.mutable; const slotv = first ? null : this.checkTdz(box, local.name); @@ -2974,7 +2996,7 @@ class LlEmitter { * the fiber eagerly to its first suspension and returns the promise); * generator bodies via theirs (which only ALLOCATES the suspended * fiber and returns the generator object) — CEmitter.callTargetC. */ - private callTarget(fnName: string): string { + callTarget(fnName: string): string { const fn = this.fnByName.get(fnName); if (fn?.generator !== undefined) return mangleGenSpawn(fnName); if (fn?.async === true) return mangleAsyncSpawn(fnName); @@ -2984,7 +3006,7 @@ class LlEmitter { /** Queue the current wasm coroutine and suspend it. The runtime decides * whether the promise waiter list or the ready FIFO owns the continuation; * LLVM keeps all live locals in the coroutine frame. */ - private emitWasiSuspend(promise: string | null): void { + emitWasiSuspend(promise: string | null): void { const coro = this.currentWasiCoro; if (coro === null) throw new InternalCompilerError("llvm emitter bug: wasm suspension outside async body"); if (promise === null) { @@ -2999,7 +3021,7 @@ class LlEmitter { /** Suspend after a target-specific runtime helper already queued the * continuation (used by module awaits, which skip the hop when settled). */ - private emitWasiSuspendPrepared(): void { + emitWasiSuspendPrepared(): void { const coro = this.currentWasiCoro; if (coro === null) throw new InternalCompilerError("llvm emitter bug: wasm suspension outside async body"); const save = this.B.tmp(); @@ -3036,7 +3058,7 @@ class LlEmitter { return; default: this.declare(`declare void @scr_gen_out_ref(ptr, ptr, ptr)`); - this.B.line(`call void @scr_gen_out_ref(ptr ${gen}, ptr ${v!.name}, ptr ${vAdapters(this, ret).release})`); + this.B.line(`call void @scr_gen_out_ref(ptr ${gen}, ptr ${v!.name}, ptr ${vAdapters(this.shapeHost, ret).release})`); return; } } @@ -3062,10 +3084,10 @@ class LlEmitter { this.B.line(`call void @scr_promise_fulfill_str(ptr ${pr}, ptr ${v!.name}) ; moves in`); break; default: { - const rc = vAdapters(this, ret); + const rc = vAdapters(this.shapeHost, ret); this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); this.B.line( - `call void @scr_promise_fulfill_ref(ptr ${pr}, ptr ${v!.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this, ret)})`, + `call void @scr_promise_fulfill_ref(ptr ${pr}, ptr ${v!.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(this.shapeHost, ret)})`, ); } } @@ -3164,7 +3186,7 @@ class LlEmitter { const slot = `%${mangleLocal(p.localId)}`; if (local.boxed) { const box = B.tmp(); - B.line(`${box} = ${boxNewCall(this, p.type)} ; ${p.name} (boxed param)`); + B.line(`${box} = ${boxNewCall(this.shapeHost, p.type)} ; ${p.name} (boxed param)`); this.boxSet(box, p.type, `%p_${mangleLocal(p.localId)}`); B.line(`store ptr ${box}, ptr ${slot}`); fnScope.push({ slot, type: p.type, boxed: true }); @@ -3262,7 +3284,7 @@ class LlEmitter { if (!ended) this.releaseScope(scope); } - private emitStmt(s: IrStmt): void { + emitStmt(s: IrStmt): void { const previous = this.B.debugLocation; this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null; try { @@ -3288,7 +3310,7 @@ class LlEmitter { const boxNew = b.local!.tdz === true && boxAccess(b.type) !== "ref" ? (this.declare(`declare ptr @scr_box_new(i32)`), `call ptr @scr_box_new(i32 3)`) - : boxNewCall(this, b.type); + : boxNewCall(this.shapeHost, b.type); const box = B.tmp(); B.line(`${box} = ${boxNew} ; let ${b.local!.name} (boxed)`); B.line(`store ptr ${box}, ptr ${b.slot}`); @@ -3611,7 +3633,7 @@ class LlEmitter { kind: "loop", brkLabel: le, contLabel: lc, - ...(s.labels !== undefined && { labels: s.labels }), + labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, finallyDepth: this.finallyStack.length, @@ -3633,7 +3655,7 @@ class LlEmitter { kind: "loop", brkLabel: le, contLabel: lc, - ...(s.labels !== undefined && { labels: s.labels }), + labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, finallyDepth: this.finallyStack.length, @@ -3691,7 +3713,7 @@ class LlEmitter { kind: "loop", brkLabel: le, contLabel: lu, - ...(s.labels !== undefined && { labels: s.labels }), + labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, finallyDepth: this.finallyStack.length, @@ -3705,7 +3727,7 @@ class LlEmitter { const slot = `%${mangleLocal(initLocal.id)}`; const fresh = B.tmp(); const old = B.tmp(); - B.line(`${fresh} = ${boxNewCall(this, initLocal.type)} ; per-iteration ${initLocal.name}`); + B.line(`${fresh} = ${boxNewCall(this.shapeHost, initLocal.type)} ; per-iteration ${initLocal.name}`); B.line(`${old} = load ptr, ptr ${slot}`); const val = this.boxGet(old, initLocal.type); // ref: +1 out this.boxSet(fresh, initLocal.type, val); // takes ownership @@ -3759,7 +3781,7 @@ class LlEmitter { kind: "loop", brkLabel: le, contLabel: lu, - ...(s.labels !== undefined && { labels: s.labels }), + labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, finallyDepth: this.finallyStack.length, @@ -3782,7 +3804,7 @@ class LlEmitter { // fresh const binding. The box takes ownership of a ref element's // +1 and is released with the iteration's scope. const box = B.tmp(); - B.line(`${box} = ${boxNewCall(this, elem)} ; per-iteration ${localInfo.name}`); + B.line(`${box} = ${boxNewCall(this.shapeHost, elem)} ; per-iteration ${localInfo.name}`); this.boxSet(box, elem, cur); B.line(`store ptr ${box}, ptr ${slot}`); this.scopes[this.scopes.length - 1]!.push({ slot, type: elem, boxed: true }); @@ -4155,7 +4177,7 @@ class LlEmitter { kind: "switch", brkLabel: end, contLabel: null, - ...(s.labels !== undefined && { labels: s.labels }), + labels: s.labels, frameDepth: this.frames.length, scopeDepth: this.scopes.length, finallyDepth: this.finallyStack.length, @@ -4193,7 +4215,7 @@ class LlEmitter { * branch and moves the result into `slot`: the chosen value's ownership * transfers, every other temp the arm allocated releases inside the * branch. The shared core of ternary/logical. CEmitter.emitBranchInto. */ - private emitBranchInto(slot: string, expr: IrExpr): void { + emitBranchInto(slot: string, expr: IrExpr): void { this.frames.push([]); const v = this.emitExpr(expr); this.moveTemp(v); @@ -4203,39 +4225,31 @@ class LlEmitter { // ── expressions ───────────────────────────────────────────────────────── - /** One audited escape hatch for the type-only boundary used by the - * extracted expression modules. LlEmitter remains the sole owner of the - * mutable emission state; the context exposes only the operations those - * modules require. */ - private expressionContext(): LlvmEmitterContext { - return this as unknown as LlvmEmitterContext; + emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue { + return emitLiteralExpr(this, e); } - private emitLiteralExpr(e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">): LlValue { - return emitLiteralExpr(this.expressionContext(), e); - } - - private emitOperatorExpr(e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">): LlValue { + emitOperatorExpr(e: ExprOf<"bin" | "unary" | "incDec" | "fieldIncDec" | "assignExpr" | "seqExpr">): LlValue { if (e.kind === "seqExpr") { // Saved operands can feed later call arguments. Transfer ownership // to this expression's frame so they survive those reads and still // die on the path that created them, including inside lazy branches. this.scopes.push([]); - const result = emitOperatorExpr(this.expressionContext(), e); + const result = emitOperatorExpr(this, e); for (const local of this.scopes.pop()!) { this.currentFrame().push({ name: local.slot, type: local.type, slot: true, ...(local.boxed ? { boxed: true } : {}) }); } return result; } - return emitOperatorExpr(this.expressionContext(), e); + return emitOperatorExpr(this, e); } - private emitControlExpr(e: ExprOf<"dynDestrCheck" | "dynIterN" | "toBool" | "logical" | "ternary" | "optChain" | "chainRecv" | "orDefault" | "nullish">): LlValue { - return emitControlExpr(this.expressionContext(), e); + emitControlExpr(e: ExprOf<"dynDestrCheck" | "dynIterN" | "toBool" | "logical" | "ternary" | "optChain" | "chainRecv" | "orDefault" | "nullish">): LlValue { + return emitControlExpr(this, e); } - private emitStringExpr(e: ExprOf<"strConcat" | "strEq" | "strCmp" | "toString" | "strIntrinsic" | "regexLit" | "templateStrings" | "regexIntrinsic">): LlValue { - return emitStringExpr(this.expressionContext(), e); + emitStringExpr(e: ExprOf<"strConcat" | "strEq" | "strCmp" | "toString" | "strIntrinsic" | "regexLit" | "templateStrings" | "regexIntrinsic">): LlValue { + return emitStringExpr(this, e); } /** @@ -4244,7 +4258,7 @@ class LlEmitter { * the statement frame while the destination relinquishes its CURRENT * value, making the snapshot unique unless a real observable alias exists. */ - private emitStringSelfConcatAssign( + emitStringSelfConcatAssign( localId: string, left: IrExpr, suffix: IrExpr, @@ -4279,233 +4293,233 @@ class LlEmitter { return result; } - private emitContainerExpr(e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">): LlValue { - return emitContainerExpr(this.expressionContext(), e); + emitContainerExpr(e: ExprOf<"arrayLit" | "arrayNewLen" | "arrayGet" | "arrayHas" | "arrayState" | "arrIntrinsic" | "bytesNew" | "bytesIntrinsic" | "mapNew" | "mapIntrinsic" | "setIntrinsic" | "setNew">): LlValue { + return emitContainerExpr(this, e); } - private emitCallExpr(e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">): LlValue { - return emitCallExpr(this.expressionContext(), e); + emitCallExpr(e: ExprOf<"call" | "ffiCall" | "closure" | "callValue" | "selfRef" | "new" | "classRef" | "newValue" | "instanceOfValue" | "promiseVoidWiden" | "upcast" | "downcast" | "instanceOf" | "virtualCall">): LlValue { + return emitCallExpr(this, e); } - private emitRecordExpr(e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">): LlValue { - return emitRecordExpr(this.expressionContext(), e); + emitRecordExpr(e: ExprOf<"fieldGet" | "recordGet" | "recordLit" | "recordClone" | "recordKeyGet" | "recordOvfKeys" | "recordOvfHas">): LlValue { + return emitRecordExpr(this, e); } - private 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 { - return emitDynamicExpr(this.expressionContext(), e); + 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 { + return emitDynamicExpr(this, e); } - private emitIntrinsicExpr(e: ExprOf<"intrinsic">): LlValue { - return emitIntrinsicExpr(this.expressionContext(), e); + emitIntrinsicExpr(e: ExprOf<"intrinsic">): LlValue { + return emitIntrinsicExpr(this, e); } - private emitSerializationExpr(e: ExprOf<"jsonStringify" | "dynCheck">): LlValue { - return emitSerializationExpr(this.expressionContext(), e); + emitSerializationExpr(e: ExprOf<"jsonStringify" | "dynCheck">): LlValue { + return emitSerializationExpr(this, e); } - private emitAsyncExpr(e: ExprOf<"yieldExpr" | "genResume" | "awaitExpr" | "awaitUnionExpr" | "newPromise" | "promiseWithResolvers">): LlValue { - return emitAsyncExpr(this.expressionContext(), e); + emitAsyncExpr(e: ExprOf<"yieldExpr" | "genResume" | "awaitExpr" | "awaitUnionExpr" | "newPromise" | "promiseWithResolvers">): LlValue { + return emitAsyncExpr(this, e); } - private emitJsInteropExpr(e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">): LlValue { - return emitJsInteropExpr(this.expressionContext(), e); + emitJsInteropExpr(e: ExprOf<"jsMarshal" | "jsOp" | "jsExit" | "jsBridgePromise">): LlValue { + return emitJsInteropExpr(this, e); } - private emitExpr(e: IrExpr): LlValue { - return emitExpr(this.expressionContext(), e); + emitExpr(e: IrExpr): LlValue { + return emitExpr(this, e); } - private emitJsMarshal(e: IrExpr & { kind: "jsMarshal" }): LlValue { - return emitJsMarshal(this.expressionContext(), e); + emitJsMarshal(e: IrExpr & { kind: "jsMarshal" }): LlValue { + return emitJsMarshal(this, e); } - private emitJsOp(e: IrExpr & { kind: "jsOp" }): LlValue { - return emitJsOp(this.expressionContext(), e); + emitJsOp(e: IrExpr & { kind: "jsOp" }): LlValue { + return emitJsOp(this, e); } - private emitJsExit(e: IrExpr & { kind: "jsExit" }): LlValue { - return emitJsExit(this.expressionContext(), e); + emitJsExit(e: IrExpr & { kind: "jsExit" }): LlValue { + return emitJsExit(this, e); } - private islandAdapter(arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string { - return islandAdapter(this.expressionContext(), arity, retKind); + islandAdapter(arity: number, retKind: "void" | "jsval" | "f64" | "bool" | "string"): string { + return islandAdapter(this, arity, retKind); } - private islandTypedAdapter(fn: IrType & { kind: "func" }): string { - return islandTypedAdapter(this.expressionContext(), fn); + islandTypedAdapter(fn: IrType & { kind: "func" }): string { + return islandTypedAdapter(this, fn); } - private dynKind(d: string): string { - return dynKind(this.expressionContext(), d); + dynKind(d: string): string { + return dynKind(this, d); } - private raceAdapterFor(from: IrType, to: IrType): string { - return raceAdapterFor(this.expressionContext(), from, to); + raceAdapterFor(from: IrType, to: IrType): string { + return raceAdapterFor(this, from, to); } - private genResultThunkFor(genT: IrType & { kind: "generator" }, recT: IrType & { kind: "record" }): string { - return genResultThunkFor(this.expressionContext(), genT, recT); + genResultThunkFor(genT: IrType & { kind: "generator" }, recT: IrType & { kind: "record" }): string { + return genResultThunkFor(this, genT, recT); } - private childExitThunkFor(param: IrType): string { - return childExitThunkFor(this.expressionContext(), param); + childExitThunkFor(param: IrType): string { + return childExitThunkFor(this, param); } - private execFileThunkFor(cbT: IrType & { kind: "func" }): string { - return execFileThunkFor(this.expressionContext(), cbT); + execFileThunkFor(cbT: IrType & { kind: "func" }): string { + return execFileThunkFor(this, cbT); } - private ipcMessageThunkFor(cbT: IrType & { kind: "func" }): string { - return ipcMessageThunkFor(this.expressionContext(), cbT); + ipcMessageThunkFor(cbT: IrType & { kind: "func" }): string { + return ipcMessageThunkFor(this, cbT); } - private ipcSendThunkFor(cbT: IrType & { kind: "func" }): string { - return ipcSendThunkFor(this.expressionContext(), cbT); + ipcSendThunkFor(cbT: IrType & { kind: "func" }): string { + return ipcSendThunkFor(this, cbT); } - private childExitSignalThunkFor(codeParam: IrType, sigParam: IrType): string { - return childExitSignalThunkFor(this.expressionContext(), codeParam, sigParam); + childExitSignalThunkFor(codeParam: IrType, sigParam: IrType): string { + return childExitSignalThunkFor(this, codeParam, sigParam); } - private childDataThunkFor(param: IrType): string { - return childDataThunkFor(this.expressionContext(), param); + childDataThunkFor(param: IrType): string { + return childDataThunkFor(this, param); } - private emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } { - return emitterFixedAdapter(this.expressionContext(), cbT); + emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } { + return emitterFixedAdapter(this, cbT); } - private wrapEmitterListener(target: string, adapterFn: string): string { - return wrapEmitterListener(this.expressionContext(), target, adapterFn); + wrapEmitterListener(target: string, adapterFn: string): string { + return wrapEmitterListener(this, target, adapterFn); } - private unwrapNullableClosure(u: string, funcTag: number): string { - return unwrapNullableClosure(this.expressionContext(), u, funcTag); + unwrapNullableClosure(u: string, funcTag: number): string { + return unwrapNullableClosure(this, u, funcTag); } - private closeBindThunkFor(cbUnion: IrType, retServer: boolean): string { - return closeBindThunkFor(this.expressionContext(), cbUnion, retServer); + closeBindThunkFor(cbUnion: IrType, retServer: boolean): string { + return closeBindThunkFor(this, cbUnion, retServer); } - private closeOverrideWrapFor(cbUnion: IrType, retServer: boolean): string { - return closeOverrideWrapFor(this.expressionContext(), cbUnion, retServer); + closeOverrideWrapFor(cbUnion: IrType, retServer: boolean): string { + return closeOverrideWrapFor(this, cbUnion, retServer); } - private streamDataAdapter(cbT: IrType & { kind: "func" }): string { - return streamDataAdapter(this.expressionContext(), cbT); + streamDataAdapter(cbT: IrType & { kind: "func" }): string { + return streamDataAdapter(this, cbT); } - private streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string { - return streamDoneFnFor(this.expressionContext(), kind, doneT); + streamDoneFnFor(kind: "w" | "f" | "d" | "t" | "l", doneT: IrType & { kind: "func" }): string { + return streamDoneFnFor(this, kind, doneT); } - private fsRenameThunkFor(cbT: IrType & { kind: "func" }): string { - return fsRenameThunkFor(this.expressionContext(), cbT); + fsRenameThunkFor(cbT: IrType & { kind: "func" }): string { + return fsRenameThunkFor(this, cbT); } - private cryptoBytesThunkFor(cbT: IrType & { kind: "func" }): string { - return cryptoBytesThunkFor(this.expressionContext(), cbT); + cryptoBytesThunkFor(cbT: IrType & { kind: "func" }): string { + return cryptoBytesThunkFor(this, cbT); } - private zlibBytesThunkFor(cbT: IrType & { kind: "func" }): string { - return zlibBytesThunkFor(this.expressionContext(), cbT); + zlibBytesThunkFor(cbT: IrType & { kind: "func" }): string { + return zlibBytesThunkFor(this, cbT); } - private streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { - return streamCbThunkFor(this.expressionContext(), kind, cbT); + streamCbThunkFor(kind: "r" | "w" | "f" | "d" | "t" | "l" | "e", cbT: IrType): string { + return streamCbThunkFor(this, kind, cbT); } - private resolveThunkFor(inner: IrType): string { - return resolveThunkFor(this.expressionContext(), inner); + resolveThunkFor(inner: IrType): string { + return resolveThunkFor(this, inner); } - private tagInSet(uName: string, tags: number[]): string { - return tagInSet(this.expressionContext(), uName, tags); + tagInSet(uName: string, tags: number[]): string { + return tagInSet(this, uName, tags); } - private arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { - return arrPush(this.expressionContext(), arr, acc, value); + arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string { + return arrPush(this, arr, acc, value); } - private emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { - return emitArrayCopyLoop(this.expressionContext(), dst, src, acc); + emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void { + return emitArrayCopyLoop(this, dst, src, acc); } - private emitStrIntrinsic(e: IrExpr & { kind: "strIntrinsic" }): LlValue { - return emitStrIntrinsic(this.expressionContext(), e); + emitStrIntrinsic(e: IrExpr & { kind: "strIntrinsic" }): LlValue { + return emitStrIntrinsic(this, e); } - private emitArrIntrinsic(e: IrExpr & { kind: "arrIntrinsic" }): LlValue { - return emitArrIntrinsic(this.expressionContext(), e); + emitArrIntrinsic(e: IrExpr & { kind: "arrIntrinsic" }): LlValue { + return emitArrIntrinsic(this, e); } - private wrapNullable(raw: string, present: string, valueType: IrType, valueTag: number, resultType: IrType & { kind: "union" }, absentTag: number): LlValue { - return wrapNullable(this.expressionContext(), raw, present, valueType, valueTag, resultType, absentTag); + wrapNullable(raw: string, present: string, valueType: IrType, valueTag: number, resultType: IrType & { kind: "union" }, absentTag: number): LlValue { + return wrapNullable(this, raw, present, valueType, valueTag, resultType, absentTag); } - private emitMapNew(e: IrExpr & { kind: "mapNew" }): LlValue { - return emitMapNew(this.expressionContext(), e); + emitMapNew(e: IrExpr & { kind: "mapNew" }): LlValue { + return emitMapNew(this, e); } - private mapSet(m: string, kAcc: "f64" | "str" | "ref", vAcc: "f64" | "bool" | "ref", key: string, value: string): void { - return mapSet(this.expressionContext(), m, kAcc, vAcc, key, value); + mapSet(m: string, kAcc: "f64" | "str" | "ref", vAcc: "f64" | "bool" | "ref", key: string, value: string): void { + return mapSet(this, m, kAcc, vAcc, key, value); } - private emitMapLikeIntrinsic( + emitMapLikeIntrinsic( e: Extract, ): LlValue { - return emitMapLikeIntrinsic(this.expressionContext(), e); + return emitMapLikeIntrinsic(this, e); } - private emitSetNew(e: IrExpr & { kind: "setNew" }): LlValue { - return emitSetNew(this.expressionContext(), e); + emitSetNew(e: IrExpr & { kind: "setNew" }): LlValue { + return emitSetNew(this, e); } - private emitStableReceiver(receiver: IrExpr, following: IrExpr[]): LlValue { - return emitStableReceiver(this.expressionContext(), receiver, following); + emitStableReceiver(receiver: IrExpr, following: IrExpr[]): LlValue { + return emitStableReceiver(this, receiver, following); } - private emitIntegerLoopIndex(expr: IrExpr): string | null { - return emitIntegerLoopIndex(this.expressionContext(), expr); + emitIntegerLoopIndex(expr: IrExpr): string | null { + return emitIntegerLoopIndex(this, expr); } - private emitBytesIndex(receiver: string, index: string, integerIndex = false): string { - return emitBytesIndex(this.expressionContext(), receiver, index, integerIndex); + emitBytesIndex(receiver: string, index: string, integerIndex = false): string { + return emitBytesIndex(this, receiver, index, integerIndex); } - private emitBytesData(receiver: string): string { - return emitBytesData(this.expressionContext(), receiver); + emitBytesData(receiver: string): string { + return emitBytesData(this, receiver); } - private emitBytesLength(elem: IrBytesElem, receiver: string, bytes: boolean): LlValue { - return emitBytesLength(this.expressionContext(), elem, receiver, bytes); + emitBytesLength(elem: IrBytesElem, receiver: string, bytes: boolean): LlValue { + return emitBytesLength(this, elem, receiver, bytes); } - private emitBytesGet(elem: IrBytesElem, receiver: string, index: string, integerIndex = false): LlValue { - return emitBytesGet(this.expressionContext(), elem, receiver, index, integerIndex); + emitBytesGet(elem: IrBytesElem, receiver: string, index: string, integerIndex = false): LlValue { + return emitBytesGet(this, elem, receiver, index, integerIndex); } - private emitToUint32(value: string, expr?: IrExpr): string { - return emitToUint32(this.expressionContext(), value, expr); + emitToUint32(value: string, expr?: IrExpr): string { + return emitToUint32(this, value, expr); } private emitBytesSet(elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void { - return emitBytesSet(this.expressionContext(), elem, receiver, index, value, integerIndex); + return emitBytesSet(this, elem, receiver, index, value, integerIndex); } - private emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { - return emitBytesIntrinsic(this.expressionContext(), e); + emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { + return emitBytesIntrinsic(this, e); } - private emitRegexIntrinsic(e: IrExpr & { kind: "regexIntrinsic" }): LlValue { - return emitRegexIntrinsic(this.expressionContext(), e); + emitRegexIntrinsic(e: IrExpr & { kind: "regexIntrinsic" }): LlValue { + return emitRegexIntrinsic(this, e); } - private emitRecordKeyGet(e: IrExpr & { kind: "recordKeyGet" }): LlValue { - return emitRecordKeyGet(this.expressionContext(), e); + emitRecordKeyGet(e: IrExpr & { kind: "recordKeyGet" }): LlValue { + return emitRecordKeyGet(this, e); } - private keyedRecordReadInto( + keyedRecordReadInto( slot: string, join: string, objName: string, @@ -4515,18 +4529,18 @@ class LlEmitter { overflowOnly: boolean, loc?: SrcLoc, ): void { - return keyedRecordReadInto(this.expressionContext(), slot, join, objName, keyName, shapeId, resultType, overflowOnly, loc); + return keyedRecordReadInto(this, slot, join, objName, keyName, shapeId, resultType, overflowOnly, loc); } - private dynPromiseAdapter(inner: IrType): string { - return dynPromiseAdapter(this.expressionContext(), inner); + dynPromiseAdapter(inner: IrType): string { + return dynPromiseAdapter(this, inner); } - private streamTypedRefCommitAdapter( + streamTypedRefCommitAdapter( t: IrType, snapshot: string, ): string { - return streamTypedRefCommitAdapter(this.expressionContext(), t, snapshot); + return streamTypedRefCommitAdapter(this, t, snapshot); } liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter { @@ -4538,7 +4552,7 @@ class LlEmitter { } const prefix = `sc_ldr_${this.liveDynRefAdapters.size}`; const adapter = streamTypedRefMaterializeAdapter( - this.expressionContext(), + this, t, { prefix, adapters: new Map() }, `${prefix}_materialize`, @@ -4547,92 +4561,92 @@ class LlEmitter { return adapter; } - private liveDynUnionRefAdapter( + liveDynUnionRefAdapter( t: IrType & { kind: "union" }, ): string { - return liveDynUnionRefAdapter(this.expressionContext(), t); + return liveDynUnionRefAdapter(this, t); } - private streamTypedRefBoxValue( + streamTypedRefBoxValue( B: BlockBuilder, t: IrType, value: string, ctx: LlStreamTypedRefContext, ): string { - return streamTypedRefBoxValue(this.expressionContext(), B, t, value, ctx); + return streamTypedRefBoxValue(this, B, t, value, ctx); } - private streamTypedRefMaterializeAdapter( + streamTypedRefMaterializeAdapter( t: IrType, ctx: LlStreamTypedRefContext, preferredSnapshot?: string, ): LlStreamTypedRefAdapter { - return streamTypedRefMaterializeAdapter(this.expressionContext(), t, ctx, preferredSnapshot); + return streamTypedRefMaterializeAdapter(this, t, ctx, preferredSnapshot); } - private streamFromArrayAdapter( + streamFromArrayAdapter( t: IrType & { kind: "array" }, ): string { - return streamFromArrayAdapter(this.expressionContext(), t); + return streamFromArrayAdapter(this, t); } - private emitWebLibCall(e: LibCallExpr): LlValue { - return emitWebLibCall(this.expressionContext(), e); + emitWebLibCall(e: LibCallExpr): LlValue { + return emitWebLibCall(this, e); } - private emitDynamicLibCall(e: LibCallExpr): LlValue { - return emitDynamicLibCall(this.expressionContext(), e); + emitDynamicLibCall(e: LibCallExpr): LlValue { + return emitDynamicLibCall(this, e); } - private emitFilesystemLibCall(e: LibCallExpr): LlValue { - return emitFilesystemLibCall(this.expressionContext(), e); + emitFilesystemLibCall(e: LibCallExpr): LlValue { + return emitFilesystemLibCall(this, e); } - private emitPathUrlLibCall(e: LibCallExpr): LlValue { - return emitPathUrlLibCall(this.expressionContext(), e); + emitPathUrlLibCall(e: LibCallExpr): LlValue { + return emitPathUrlLibCall(this, e); } - private emitPrimitiveLibCall(e: LibCallExpr): LlValue { - return emitPrimitiveLibCall(this.expressionContext(), e); + emitPrimitiveLibCall(e: LibCallExpr): LlValue { + return emitPrimitiveLibCall(this, e); } - private emitChildProcessLibCall(e: LibCallExpr): LlValue { - return emitChildProcessLibCall(this.expressionContext(), e); + emitChildProcessLibCall(e: LibCallExpr): LlValue { + return emitChildProcessLibCall(this, e); } - private emitAsyncContextLibCall(e: LibCallExpr): LlValue { - return emitAsyncContextLibCall(this.expressionContext(), e); + emitAsyncContextLibCall(e: LibCallExpr): LlValue { + return emitAsyncContextLibCall(this, e); } - private emitProcessLibCall(e: LibCallExpr): LlValue { - return emitProcessLibCall(this.expressionContext(), e); + emitProcessLibCall(e: LibCallExpr): LlValue { + return emitProcessLibCall(this, e); } - private emitErrorsEventsLibCall(e: LibCallExpr): LlValue { - return emitErrorsEventsLibCall(this.expressionContext(), e); + emitErrorsEventsLibCall(e: LibCallExpr): LlValue { + return emitErrorsEventsLibCall(this, e); } - private emitStreamLibCall(e: LibCallExpr): LlValue { - return emitStreamLibCall(this.expressionContext(), e); + emitStreamLibCall(e: LibCallExpr): LlValue { + return emitStreamLibCall(this, e); } - private emitNetworkHttpLibCall(e: LibCallExpr): LlValue { - return emitNetworkHttpLibCall(this.expressionContext(), e); + emitNetworkHttpLibCall(e: LibCallExpr): LlValue { + return emitNetworkHttpLibCall(this, e); } - private emitAssertInspectLibCall(e: LibCallExpr): LlValue { - return emitAssertInspectLibCall(this.expressionContext(), e); + emitAssertInspectLibCall(e: LibCallExpr): LlValue { + return emitAssertInspectLibCall(this, e); } - private emitIoLibCall(e: LibCallExpr): LlValue { - return emitIoLibCall(this.expressionContext(), e); + emitIoLibCall(e: LibCallExpr): LlValue { + return emitIoLibCall(this, e); } - private emitGenericLibCall(e: LibCallExpr): LlValue { - return emitGenericLibCall(this.expressionContext(), e); + emitGenericLibCall(e: LibCallExpr): LlValue { + return emitGenericLibCall(this, e); } - private emitLibCall(e: LibCallExpr): LlValue { - return emitLibCall(this.expressionContext(), e); + emitLibCall(e: LibCallExpr): LlValue { + return emitLibCall(this, e); } } diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 4ee85730..0d3acb2b 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -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}`); } diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index 37e5a0c0..f0af0b33 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -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"; diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index 8e377a99..8fa6efea 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -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`, `}`, ``); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 03c6fdaf..79fb53d7 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -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)`); diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts index 3184b1cd..3ca0082c 100644 --- a/packages/compiler/src/backend/llvm/expr-context.ts +++ b/packages/compiler/src/backend/llvm/expr-context.ts @@ -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; unions?: Map; } - -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; - 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; - 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; - 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; - 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): 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; - ffiCallbackAdapter(binding: string, id: string): FfiCallbackAdapter; - ffiHasRetainedCallback: boolean; - fnByName: Map; - fnValues: Set; - 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; - 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; - liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter; - liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string; - liveDynUnionRefAdapters: Map; - 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; - mayThrowMethods: Set; - 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; - recordFieldPtr(objName: string, shapeId: string, field: string): { ptr: string; type: IrType }; - recordOvfPtr(objName: string, shapeId: string): string; - recordShape(shapeId: string): IrRecordShape; - recordsById: Map; - regexInstances: Map; - releaseFrame(frame: LlValue[]): void; - releaseValue(name: string, type: IrType): void; - resolveThunkDefs: string[]; - resolveThunkFor(inner: IrType): string; - resolveThunks: Map; - 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; - 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; - 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; - 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; -} diff --git a/packages/compiler/src/backend/llvm/expr-dispatch.ts b/packages/compiler/src/backend/llvm/expr-dispatch.ts index b4b8dafb..1729cae6 100644 --- a/packages/compiler/src/backend/llvm/expr-dispatch.ts +++ b/packages/compiler/src/backend/llvm/expr-dispatch.ts @@ -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 { diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 3523ddbf..354165fe 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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)`, ); diff --git a/packages/compiler/src/backend/llvm/expr-island.ts b/packages/compiler/src/backend/llvm/expr-island.ts index 078c47a8..5b527ea1 100644 --- a/packages/compiler/src/backend/llvm/expr-island.ts +++ b/packages/compiler/src/backend/llvm/expr-island.ts @@ -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)`, diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 4dcd5059..e76677df 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -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 }], diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index ec9813bd..279aebd2 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -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"; diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 3e230c7a..80dec488 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -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"; diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 53114540..656ead42 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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(); + 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( diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts index 72f56b33..ed6c3e7d 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts @@ -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`, `}`, ``); diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 31bdb66c..8cdef62d 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 7ef3c56e..4d51d55d 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -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 = 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( diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 83e155d7..64aa59bd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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 | 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); } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e94eb566..45e7c9ac 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3745,7 +3745,7 @@ export class Lowerer { this.diags.length > 0 ? null : { - irVersion: 12, + irVersion: 13, sourceFile: this.entry.fileName, functions, classes: artifacts.classes, diff --git a/packages/compiler/src/ir/constant-tables.test.ts b/packages/compiler/src/ir/constant-tables.test.ts index ef9fe947..34fdd6d7 100644 --- a/packages/compiler/src/ir/constant-tables.test.ts +++ b/packages/compiler/src/ir/constant-tables.test.ts @@ -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 }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 877b0993..e4ba42c1 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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 diff --git a/packages/compiler/src/ir/scalar-records.test.ts b/packages/compiler/src/ir/scalar-records.test.ts index 1d81cf47..40b267b7 100644 --- a/packages/compiler/src/ir/scalar-records.test.ts +++ b/packages/compiler/src/ir/scalar-records.test.ts @@ -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: [ { diff --git a/packages/compiler/src/ir/serialize.test.ts b/packages/compiler/src/ir/serialize.test.ts new file mode 100644 index 00000000..bc4660cc --- /dev/null +++ b/packages/compiler/src/ir/serialize.test.ts @@ -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"); +}); diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts index 10d85bf8..c175ce8e 100644 --- a/packages/compiler/src/ir/serialize.ts +++ b/packages/compiler/src/ir/serialize.ts @@ -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; diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 936c3cea..2ae1c3e0 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -35,14 +35,14 @@ function numericReadModule(overrides: Partial 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 }] }, diff --git a/packages/compiler/src/library/int-infer.test.ts b/packages/compiler/src/library/int-infer.test.ts index ad748bf4..65717bec 100644 --- a/packages/compiler/src/library/int-infer.test.ts +++ b/packages/compiler/src/library/int-infer.test.ts @@ -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", diff --git a/packages/compiler/src/library/library-cache.test.ts b/packages/compiler/src/library/library-cache.test.ts index c816c739..15190bc8 100644 --- a/packages/compiler/src/library/library-cache.test.ts +++ b/packages/compiler/src/library/library-cache.test.ts @@ -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", diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index e0e7f6bd..8ed2da02 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -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 }], diff --git a/packages/compiler/test/emit-c.test.ts b/packages/compiler/test/emit-c.test.ts index c8e14d2f..7b15ac3b 100644 --- a/packages/compiler/test/emit-c.test.ts +++ b/packages/compiler/test/emit-c.test.ts @@ -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: [ diff --git a/packages/compiler/test/fixtures/fib-ir.ts b/packages/compiler/test/fixtures/fib-ir.ts index 5fe9a9be..3b5a43cb 100644 --- a/packages/compiler/test/fixtures/fib-ir.ts +++ b/packages/compiler/test/fixtures/fib-ir.ts @@ -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: [ diff --git a/packages/compiler/test/fromcharcode-emission.test.ts b/packages/compiler/test/fromcharcode-emission.test.ts index f446517d..efaa28cd 100644 --- a/packages/compiler/test/fromcharcode-emission.test.ts +++ b/packages/compiler/test/fromcharcode-emission.test.ts @@ -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: { diff --git a/packages/compiler/test/ir.test.ts b/packages/compiler/test/ir.test.ts index cbea9f61..32da863e 100644 --- a/packages/compiler/test/ir.test.ts +++ b/packages/compiler/test/ir.test.ts @@ -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/); }); diff --git a/packages/compiler/test/llvm-lib-dispatch.test.ts b/packages/compiler/test/llvm-lib-dispatch.test.ts index 1c67d613..ccda63d4 100644 --- a/packages/compiler/test/llvm-lib-dispatch.test.ts +++ b/packages/compiler/test/llvm-lib-dispatch.test.ts @@ -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: [{ diff --git a/packages/compiler/test/string-accumulation-emission.test.ts b/packages/compiler/test/string-accumulation-emission.test.ts index 27e78c27..32b8c425 100644 --- a/packages/compiler/test/string-accumulation-emission.test.ts +++ b/packages/compiler/test/string-accumulation-emission.test.ts @@ -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 }], diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 1e6ec220..479065a0 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -7595,6 +7595,18 @@ ], "diags": [] }, + "/tests/corpus/3119-runtime-optional-spread.ts": { + "order": [ + "/tests/corpus/3119-runtime-optional-spread.ts" + ], + "diags": [] + }, + "/tests/corpus/3120-ir-nonfinite-numbers.ts": { + "order": [ + "/tests/corpus/3120-ir-nonfinite-numbers.ts" + ], + "diags": [] + }, "/tests/corpus/400-fib.ts": { "order": [ "/tests/corpus/400-fib.ts" @@ -9796,6 +9808,12 @@ ], "diags": [] }, + "/tests/diagnostics/spread-runtime-optional.ts": { + "order": [ + "/tests/diagnostics/spread-runtime-optional.ts" + ], + "diags": [] + }, "/tests/diagnostics/statements.ts": { "order": [ "/tests/diagnostics/statements.ts" diff --git a/tests/corpus/3119-runtime-optional-spread.ts b/tests/corpus/3119-runtime-optional-spread.ts new file mode 100644 index 00000000..7e24b1f0 --- /dev/null +++ b/tests/corpus/3119-runtime-optional-spread.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); +} diff --git a/tests/corpus/3120-ir-nonfinite-numbers.ts b/tests/corpus/3120-ir-nonfinite-numbers.ts new file mode 100644 index 00000000..cea5e633 --- /dev/null +++ b/tests/corpus/3120-ir-nonfinite-numbers.ts @@ -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(); +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 })); diff --git a/tests/diagnostics/spread-runtime-optional.ts b/tests/diagnostics/spread-runtime-optional.ts new file mode 100644 index 00000000..ddf3ef44 --- /dev/null +++ b/tests/diagnostics/spread-runtime-optional.ts @@ -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); diff --git a/tests/fixtures/self-hosting/llvm-emitter.ts b/tests/fixtures/self-hosting/llvm-emitter.ts new file mode 100644 index 00000000..100fa9ee --- /dev/null +++ b/tests/fixtures/self-hosting/llvm-emitter.ts @@ -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(); +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; + } +} diff --git a/tests/fixtures/self-hosting/serialize.ts b/tests/fixtures/self-hosting/serialize.ts index c27dc887..97a358e0 100644 --- a/tests/fixtures/self-hosting/serialize.ts +++ b/tests/fixtures/self-hosting/serialize.ts @@ -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; diff --git a/tests/harness/__snapshots__/spread-runtime-optional.ts.txt b/tests/harness/__snapshots__/spread-runtime-optional.ts.txt new file mode 100644 index 00000000..c3745348 --- /dev/null +++ b/tests/harness/__snapshots__/spread-runtime-optional.ts.txt @@ -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); \ No newline at end of file diff --git a/tests/harness/self-hosting-llvm-embedding.test.ts b/tests/harness/self-hosting-llvm-embedding.test.ts new file mode 100644 index 00000000..77926408 --- /dev/null +++ b/tests/harness/self-hosting-llvm-embedding.test.ts @@ -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"); +}); diff --git a/tests/harness/self-hosting-llvm-embedding.ts b/tests/harness/self-hosting-llvm-embedding.ts new file mode 100644 index 00000000..7f2e29cd --- /dev/null +++ b/tests/harness/self-hosting-llvm-embedding.ts @@ -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; +} diff --git a/tests/harness/self-hosting-llvm-emitter-cases.ts b/tests/harness/self-hosting-llvm-emitter-cases.ts new file mode 100644 index 00000000..c8d5f486 --- /dev/null +++ b/tests/harness/self-hosting-llvm-emitter-cases.ts @@ -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 { + 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; +} diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts new file mode 100644 index 00000000..3a3a9ebf --- /dev/null +++ b/tests/harness/self-hosting-llvm-emitter.test.ts @@ -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>; + 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 => { + 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); +} diff --git a/tests/harness/self-hosting-serialization.test.ts b/tests/harness/self-hosting-serialization.test.ts index e3c31485..fce8b1c0 100644 --- a/tests/harness/self-hosting-serialization.test.ts +++ b/tests/harness/self-hosting-serialization.test.ts @@ -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 }); } diff --git a/tests/harness/self-hosting-validator-cases.ts b/tests/harness/self-hosting-validator-cases.ts index 9a33c089..b1091db5 100644 --- a/tests/harness/self-hosting-validator-cases.ts +++ b/tests/harness/self-hosting-validator-cases.ts @@ -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 }], });