From 83476d0c9587b6be8a8b585a024e35e5fa12ef12 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 29 Sep 2026 20:52:09 -0500 Subject: [PATCH] Compile three.js math modules with LLVM - Run published three.js vector, matrix, and quaternion operations natively and as Wasm without a JavaScript engine. - Preserve shared class prototype data and JavaScript overload arguments. - Support Math function values and distinct npm import and require entries. --- package.json | 1 + .../src/backend/llvm/expr-stream-bridges.ts | 20 ++- .../lowering/class-dynamic-dispatch.ts | 21 +++- .../src/frontend/lowering/class-prototypes.ts | 67 ++++++++++ .../src/frontend/lowering/lower-builtins.ts | 2 +- .../src/frontend/lowering/lower-calls.ts | 21 +++- .../src/frontend/lowering/lower-exprs.ts | 20 +++ .../src/frontend/lowering/lower-island.ts | 15 ++- .../src/frontend/lowering/lower-stmts.ts | 18 ++- .../compiler/src/frontend/lowering/lowerer.ts | 116 +++++++++++++----- .../compiler/src/frontend/npm-static-prune.ts | 4 +- packages/compiler/src/frontend/npm-static.ts | 18 +-- packages/compiler/src/frontend/program.ts | 17 +-- packages/compiler/src/frontend/resolve.ts | 7 +- packages/compiler/src/ir/ir.ts | 3 + packages/compiler/src/ir/validate.test.ts | 17 +++ packages/compiler/src/ir/validate.ts | 6 + packages/runtime/src/scr_json.c | 10 +- pnpm-lock.yaml | 8 ++ tests/corpus/class-brand-overload.js | 15 +++ tests/corpus/class-prototype-data.js | 38 ++++++ tests/corpus/js-omitted-typed-arguments.js | 33 +++++ tests/corpus/math-function-values.js | 13 ++ .../diagnostics/class-prototype-reflection.ts | 10 ++ tests/diagnostics/stdlib-fence.ts | 2 +- tests/diagnostics/stdlib.ts | 4 +- tests/fixtures/three/math.mjs | 31 +++++ .../class-prototype-reflection.ts.txt | 27 ++++ .../harness/__snapshots__/stdlib-fence.ts.txt | 7 -- tests/harness/__snapshots__/stdlib.ts.txt | 9 +- tests/harness/npm-static.test.ts | 33 +++++ tests/harness/three.test.ts | 57 +++++++++ 32 files changed, 576 insertions(+), 94 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/class-prototypes.ts create mode 100644 tests/corpus/class-brand-overload.js create mode 100644 tests/corpus/class-prototype-data.js create mode 100644 tests/corpus/js-omitted-typed-arguments.js create mode 100644 tests/corpus/math-function-values.js create mode 100644 tests/diagnostics/class-prototype-reflection.ts create mode 100644 tests/fixtures/three/math.mjs create mode 100644 tests/harness/__snapshots__/class-prototype-reflection.ts.txt create mode 100644 tests/harness/three.test.ts diff --git a/package.json b/package.json index 9593cbfe5..d0f57f78a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "effect": "3.22.2", "eslint": "^9.20.0", "js-yaml": "4.3.0", + "three": "0.186.1", "tsx": "^4.19.0", "typescript": "5.9.3", "typescript-eslint": "^8.24.0", diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 7cd571f49..ba5f3db1d 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -3,7 +3,7 @@ import { typedRefConstructor } from "./shapes.js"; import { InternalCompilerError } from "../../errors.js"; import { streamTypedRefEligible } from "../../ir/analysis.js"; import { type IrType, DYN_CLASS_PROPERTIES, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js"; -import { mangleRecordStruct } from "../mangle.js"; +import { mangleFunction, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; import { classFieldIndex, classStructSym } from "./classes.js"; import { llvmCommentText } from "./common.js"; @@ -201,6 +201,15 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext, ` br i1 %bag_pending, label %bag_fail, label %bag_keys`, `bag_fail:`, ` call void @scr_dyn_release_v(ptr %bag)`, ` br label %done`, `bag_keys:`, ); + if (meta.def.prototypeDataHelper) { + host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`); + lines.push( + ` %prototype = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`, + ` %with_prototype = call ptr @scr_dyn_set_prototype(ptr %bag, ptr %prototype)`, + ` call void @scr_dyn_release_v(ptr %prototype)`, + ` call void @scr_dyn_release_v(ptr %with_prototype)`, + ); + } fields.forEach((field, index) => lines.push( ` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`, ` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`, @@ -564,6 +573,15 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext, host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`); const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_obj()`); + if (meta.def.prototypeDataHelper) { + host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`); + host.declare(`declare void @scr_dyn_release_v(ptr)`); + const prototype = B.tmp(), attached = B.tmp(); + B.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`); + B.line(`${attached} = call ptr @scr_dyn_set_prototype(ptr ${out}, ptr ${prototype})`); + B.line(`call void @scr_dyn_release_v(ptr ${prototype})`); + B.line(`call void @scr_dyn_release_v(ptr ${attached})`); + } for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) { const { index } = classFieldIndex(meta, field.name); const fieldPtr = B.tmp(); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 8b5095149..24492c6e3 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -5,6 +5,7 @@ import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js"; import { implicitDefaultInstance, type ParamShape } from "./lower-calls.js"; import { accessorCall, findGenericMethodOn, findMethodOn, upcastTo, type ClassInfo } from "./lower-classes.js"; +import { classPrototypeData, hasClassPrototypeData } from "./class-prototypes.js"; type Invoke = Extract; interface Dispatch { @@ -31,6 +32,7 @@ export class ClassDynamicDispatch { private readonly computed = new Map & { keyLocal: string; branchIndex: number }>(); private propertyBag: IrFunction | null = null; private readonly bagClasses = new Set(); + private readonly bagInitializers = new Map>(); private readonly generated = new Set(); process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean { @@ -82,9 +84,17 @@ export class ClassDynamicDispatch { changed = true; } for (const className of this.boxed) { - if (this.bagClasses.has(className)) continue; const info = lowerer.classes.get(className); if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue; + const existingInit = this.bagInitializers.get(className); + if (existingInit && existingInit.value.kind === "dynObjLit" && hasClassPrototypeData(info)) { + const prototype = classPrototypeData(lowerer, info, existingInit.loc); + if (prototype) { + existingInit.value = { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc: existingInit.loc }; + changed = true; + } + } + if (this.bagClasses.has(className)) continue; this.bagClasses.add(className); this.ensurePropertyBag(info); const loc = this.propertyBag.loc; @@ -92,11 +102,18 @@ export class ClassDynamicDispatch { const value = varRef("p.0", DYN, loc); const receiver: IrExpr = { kind: "dynCheck", value, type, loc }; const bag: IrExpr = { kind: "fieldGet", obj: receiver, className, field: PROPERTY_BAG, type: DYN, loc }; + const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc) : null; + const initialize: Extract = { + kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: prototype + ? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc } + : { kind: "dynObjLit", fields: [], type: DYN, loc }, loc, + }; + this.bagInitializers.set(className, initialize); this.propertyBag.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [ { kind: "if", cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, then: [ - { kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc }, + initialize, ], else_: null, loc }, { kind: "return", value: bag, loc }, ], else_: null, loc, diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts new file mode 100644 index 000000000..99c6edca1 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -0,0 +1,67 @@ +import * as ts from "../ts7/adapter.js"; +import { BOOL, DYN, type IrExpr, type IrFunction, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { locOf } from "../program.js"; +import { exactClassOfReceiver, findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Data added to a top-level class prototype has shared identity and remains + * separate from instance fields. Allocate lazily so inheritance and module + * initialization order do not require hoisting source assignments. */ +export function classPrototypeData(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrExpr | null { + if (info.localClass || info.mixinInstance || info.generic || info.genericInstance || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null; + if (info.def.prototypeDataHelper === undefined) { + const base = info.base ? classPrototypeData(lowerer, info.base, loc) : null; + if (info.base && base === null) return null; + const name = `%prototype.data.${info.def.name}`; + const globalId = `%g.${name}`; + const readyId = `${globalId}.ready`; + info.def.prototypeDataHelper = name; + const className = info.def.jsName ?? info.def.name; + lowerer.globalsList.push({ id: globalId, name: `${className}.prototype`, type: DYN, mutable: true }); + lowerer.globalsList.push({ id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true }); + const value = varRef(globalId, DYN, loc); + const helper: IrFunction = { + name, params: [], returnType: DYN, locals: [], loc, + body: [ + { kind: "if", cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, then: [ + { kind: "assign", localId: globalId, value: base + ? { kind: "libCall", fn: "dyn.objCreate", args: [base], type: DYN, loc } + : { kind: "dynObjLit", fields: [], type: DYN, loc }, loc }, + { kind: "assign", localId: readyId, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }, + ], else_: null, loc }, + { kind: "return", value, loc }, + ], + }; + lowerer.liftedFns.push(helper); + } + return { kind: "call", callee: info.def.prototypeDataHelper, args: [], type: DYN, loc }; +} + +export function hasClassPrototypeData(info: ClassInfo): boolean { + return info.def.prototypeDataHelper !== undefined || (info.base !== null && hasClassPrototypeData(info.base)); +} + +export function isCompiledPrototypeMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + return name === "constructor" || name === "__proto__" || + !!findMethodOn(lowerer, info, name) || !!findGenericMethodOn(lowerer, info, name) || + !!findMethodOn(lowerer, info, `get:${name}`) || !!findMethodOn(lowerer, info, `set:${name}`); +} + +/** Compiled methods still live in vtables. Keep prototype reflection and + * method replacement fenced until those descriptors have a native view. */ +export function lowerClassPrototypeData(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { + if (expr.questionDotToken || expr.name.text !== "prototype") return null; + const info = exactClassOfReceiver(lowerer, expr.expression); + if (!info) return null; + const parent = expr.parent; + const name = ts.isPropertyAccessExpression(parent) && parent.expression === expr ? parent.name.text + : ts.isElementAccessExpression(parent) && parent.expression === expr && parent.argumentExpression && ts.isStringLiteral(parent.argumentExpression) + ? parent.argumentExpression.text : null; + if (name === null || isCompiledPrototypeMember(lowerer, info, name)) { + lowerer.unsupported("SC1090", expr, "class prototype reflection and compiled method replacement (named prototype data properties compile)"); + } + const value = classPrototypeData(lowerer, info, locOf(expr)); + if (value === null) lowerer.unsupported("SC1090", expr, "prototype data on local, generic, mixin, or runtime-provided classes"); + return value; +} diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index 3b1272b5c..a720faf49 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -512,7 +512,7 @@ export function isNativeFfiRequire(lowerer: Lowerer, expr: ts.Expression | undef const cr = createRequireSpecOf(lowerer, call); if (cr === null || cr.spec === null || canonicalBuiltinModule(cr.spec) !== null) return null; const dep = resolveImport(lowerer.program, cr.baseFile, cr.spec) ?? - npmStaticDepSf7(lowerer.program, cr.baseFile, cr.spec); + npmStaticDepSf7(lowerer.program, cr.baseFile, cr.spec, "require"); if (dep === null || dep.fileName.endsWith(".json")) return null; return { spec: cr.spec, baseFile: cr.baseFile, dep }; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 997dabe6c..e1198338c 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -8,7 +8,7 @@ import { AstNode } from "../ts7/ast-node.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { lowerGenMethodCall } from "./lower-generators.js"; -import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, NULL_T, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; +import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, NULL_T, STRING, SYMBOL_T, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isDynTypedRefType, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js"; import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js"; import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js"; @@ -604,7 +604,7 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration, }); const fixed = positional.map((shape, i): IrExpr => { if (i < passed.length) return passed[i]!; - if (shape.mode === "omittable") return shape.callDefault ?? lowerer.undefinedArgFor(shape.type, loc, blame); + if (shape.mode === "omittable" || lowerer.bareUndefinedArmedUnion(shape.type)) return shape.callDefault ?? lowerer.undefinedArgFor(shape.type, loc, blame); if (shape.type.kind === "dyn") return { kind: "dynFrom", value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }, type: DYN, loc }; lowerer.unsupported("SC1090", blame, "this call form"); }); @@ -630,8 +630,17 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration, "spread arguments into fixed parameter positions (a spread can only fill a rest parameter)", ); } - if (arg) return lowerer.lowerExprExpecting(arg, shape.type); - if (shape.mode !== "omittable") { + if (arg) { + let operand = arg; + while (ts.isParenthesizedExpression(operand)) operand = operand.expression; + if (ts.isVoidExpression(operand)) { + const absent = omittedArgFor(lowerer, shape.type, loc); + const effect = lowerStaticallyUndefinedArgument(lowerer, operand); + if (absent && effect) return defaultAfterUndefined(effect, absent); + } + return lowerer.lowerExprExpecting(arg, shape.type); + } + if (shape.mode !== "omittable" && !lowerer.bareUndefinedArmedUnion(shape.type)) { // A missing argument for a CHECKED-DYNAMIC param (an implicit-any // JS signature called short — `mustCall(fn)` with `expected` // omitted): JS fills undefined, and the dyn slot holds exactly @@ -9389,7 +9398,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression // A CHECKED-DYNAMIC receiver (the JS file-scope object-literal // identity story): the runtime dyn probe — OBJ member presence, ARR // index bounds, Node's ToObject TypeError on nullish. - if (probed?.type.kind === "dyn" || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) { + if (probed?.type.kind === "dyn" || (probed && isDynTypedRefType(probed.type)) || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) { const loc = locOf(call); const receiver = lowerer.coerceToExpected(probed, DYN); const key = ownPropertyKey(lowerer, lowerer.lowerExpr(keyNode)); @@ -9472,7 +9481,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression const isPrimitive = probed !== null && probed !== undefined && (isUnitType(probed.type) || probed.type.kind === "string" || probed.type.kind === "f64" || probed.type.kind === "bool"); - if (isDyn || isPrimitive || isFunction) { + if (isDyn || isPrimitive || isFunction || (probed && isDynTypedRefType(probed.type))) { const fn = member === "keys" ? "dyn.objKeys" : member === "values" ? "dyn.objValues" : "dyn.objEntries"; let v = lowerer.lowerExpr(argNode); if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) }; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 08f793779..5f3c73743 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -50,6 +50,7 @@ import { recordTextCodecClass } from "../../ir/ir.js"; import { classSymbolKeyOf } from "./symbol-fields.js"; import { lowerClassMethodValue } from "./class-method-values.js"; import { lowerGlobalValue } from "./lower-global-value.js"; +import { lowerClassPrototypeData } from "./class-prototypes.js"; /** An assignable `obj.field` target — a class field, a record field, or a * class ACCESSOR property (reads become getter calls, writes setter calls; @@ -1511,6 +1512,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + const prototypeData = lowerClassPrototypeData(lowerer, expr); + if (prototypeData) return prototypeData; // `super.x`: the base chain's GETTER, called directly (super // dispatch is static in JS — never through the dynamic class). // super.method() calls are routed at the call site; a bare super @@ -2150,6 +2153,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { `reading the abstract property '${expr.name.text}' through a '${lowerer.checker.typeToString(lowerer.typeOf(expr.expression))}'-typed receiver (abstract property declarations are erased at runtime, so no shared slot exists — type the receiver as the concrete class, or declare an abstract getter instead)`, ); } + if (isDynTypedRefType(recvLowered.type)) { + const info = lowerer.classes.get(recvLowered.type.className); + if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) { + return lowerer.maybeNarrow({ + kind: "dynKeyGet", value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc, + }, expr); + } + } lowerer.unsupported( "SC1090", expr, @@ -9656,6 +9668,14 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE const key: IrExpr = { kind: "strLit", value: field, type: STRING, loc: locOf(expr.name) }; const keyed = lowerUnionKeyedRead(lowerer, expr, value.type.unionId, value, key, field); if (keyed) return keyed; + // JavaScript overloads commonly inspect a class brand on a scalar-or- + // instance argument (e.g. Matrix4.makeTranslation). Preserve primitive + // missing-property semantics and dispatch class data through its native + // capsule instead of demanding a field shared by every union arm. + if (isJsSourceFile(expr.getSourceFile()) && def.arms.some(isDynTypedRefType) && lowerer.dynConvertible(value.type)) { + return { kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), key, + ...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}), type: DYN, loc: locOf(expr) }; + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index ee93cc662..15024cd20 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -3435,11 +3435,8 @@ export function lowerStaticReadableStreamReaderCall( return finish(lowerer.jsvalIn(lowerer.lowerExpr(access.expression), access.expression), entry); } -/** Canonical Math constant property reads become typed numeric - * literals. Remaining Math properties retain the island/fence path. Math - * methods referenced without a call are rejected specifically (no value form - * exists, --dynamic or not). Null for non-Math receivers (the property chain - * keeps trying). */ +/** Math constants and fixed-arity numeric functions have native value forms. + * Remaining properties retain the island/fence path. */ export function lowerMathProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { const member = lowerer.stdlibGlobalMember(expr, "Math"); if (member === null) return null; @@ -3448,6 +3445,14 @@ export function lowerStaticReadableStreamReaderCall( if (staticValue !== undefined) { return { kind: "numLit", value: staticValue, type: F64, loc }; } + const native = own(STATIC_MATH_FNS, member); + if (native && member !== "min" && member !== "max" && member !== "hypot") { + const params = Array.from({ length: native.arity }, () => F64); + return lowerer.lowerNativeCallableValue({ + fn: native.fn, params, result: F64, + valueParams: params.map((type) => ({ mode: "required", type })), + }, `Math.${member}`, loc); + } const propType = own(ISLAND_SURFACE.math.props, member); if (propType !== undefined) { lowerer.requireDynamicApi(`'Math.${member}'`, expr); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 227452cd6..a562c4595 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -9,7 +9,7 @@ import type { Lowerer } from "./lowerer.js"; import { arrayValueRead, arrayValueStore, arrayValueType, unionArrayValueRead } from "./array-values.js"; import { lowerForAwaitGenerator, lowerForOfGenerator, lowerYieldStarStatement, type GenType } from "./lower-generators.js"; import { lowerForAwaitBuiltin } from "./lower-async-iteration.js"; -import { BOOL, BYTES_U8, CAUGHT, DYN, F64, type IrExpr, type IrGlobal, type IrLocal, type IrStmt, type IrType, JSVAL, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; +import { BOOL, BYTES_U8, CAUGHT, DYN, F64, type IrExpr, type IrGlobal, type IrLocal, type IrStmt, type IrType, JSVAL, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isDynTypedRefType, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, staticImportNamespaceType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isEsModuleStamp, isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js"; @@ -22,6 +22,7 @@ import { type ForOfIterProjection, lowerForOfArrayIter, lowerForOfMap, lowerForO import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, nullishExprUnitOf, nullishGenericBindingUnitOf, recordKeysArrayCall, registerOverloadedCallableAlias } from "./lower-calls.js"; import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js"; import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js"; +import { isCompiledPrototypeMember } from "./class-prototypes.js"; import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js"; import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js"; import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js"; @@ -5314,6 +5315,21 @@ function isStrictDelete(node: ts.DeleteExpression): boolean { `writing the abstract property '${expr.left.name.text}' through a '${lowerer.checker.typeToString(lowerer.typeOf(expr.left.expression))}'-typed receiver (abstract property declarations are erased at runtime, so no shared slot exists — type the receiver as the concrete class, or declare an abstract accessor pair instead)`, ); } + if (!expr.left.questionDotToken) { + const recv = tryLowerExpression(lowerer, expr.left.expression); + if (recv && isDynTypedRefType(recv.type)) { + const info = lowerer.classes.get(recv.type.className); + if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream && + !isCompiledPrototypeMember(lowerer, info, expr.left.name.text)) { + const loc = locOf(expr); + return { kind: "exprStmt", expr: { + kind: "libCall", fn: "dyn.keySet", args: [lowerer.coerceToExpected(recv, DYN), + { kind: "strLit", value: expr.left.name.text, type: STRING, loc }, lowerer.lowerExprExpecting(expr.right, DYN)], + type: VOID, loc, + }, loc }; + } + } + } // Dot WRITE to an undeclared key of an index-signature shape: // the same deliberate fence as the dotted read, with the same // fix in the message (brackets are the index-signature form). diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 7ce6fe781..5834af8fa 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2481,7 +2481,7 @@ export class Lowerer { const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { return resolveImport(this.program, decl.getSourceFile(), directSpec) ?? - npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec); + npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require"); } return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null; } @@ -2781,7 +2781,7 @@ export class Lowerer { // those globals stay uninitialized: the dep's module body would never // run. const dep = resolveImport(this.program, node.getSourceFile(), spec) ?? - npmStaticDepSf7(this.program, node.getSourceFile(), spec); + npmStaticDepSf7(this.program, node.getSourceFile(), spec, "require"); if (!dep || dep.fileName.endsWith(".json")) return null; if (this.asyncInitFiles.has(dep)) { this.unsupported( @@ -2872,6 +2872,9 @@ export class Lowerer { method?: ClassMethodSignature; }; const signatureBySymbol = new Map(); + const methodFamilies = new Map(); + const familyBySymbol = new Map(); + const optionalMethodFamilies = new Set(); for (const [symbol, sig] of this.fnSigsBySymbol) signatureBySymbol.set(symbol, { params: sig.params, returnType: sig.returnType, top: sig }); for (const decl of fnDecls) { const symbol = declSymbolOf(this, decl); @@ -2893,7 +2896,16 @@ export class Lowerer { if (!member.name || !(ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))) continue; const symbol = symbolOf(member.name); const sig = info.methods.get(mName); - if (symbol && sig) signatureBySymbol.set(symbol, { params: sig.params, returnType: sig.ret, method: sig }); + if (symbol && sig) { + signatureBySymbol.set(symbol, { params: sig.params, returnType: sig.ret, method: sig }); + let root = info; + for (let base = info.base; base; base = base.base) if (base.methods.has(mName)) root = base; + const key = `${root.def.name}:${mName}`; + const family = methodFamilies.get(key) ?? []; + family.push(symbol); + methodFamilies.set(key, family); + familyBySymbol.set(symbol, family); + } } } const peel = (node: ts.Expression): ts.Expression => { @@ -2903,6 +2915,20 @@ export class Lowerer { } return e; }; + const callableSymbolOf = (node: ts.Expression): ts.Symbol | null => { + let symbol = symbolOf(ts.isPropertyAccessExpression(node) ? node.name : node); + const seen = new Set(); + while (symbol && !signatureBySymbol.has(symbol) && !seen.has(symbol)) { + seen.add(symbol); + const declaration = this.checker.valueDeclarationOf(symbol); + if (declaration && ts.isMethodDeclaration(declaration)) { + symbol = symbolOf(declaration.name); + } else if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isIdentifier(peel(declaration.initializer))) { + symbol = symbolOf(peel(declaration.initializer)); + } else break; + } + return symbol; + }; const explicitlyNonNull = (node: ts.Expression): boolean => { let e = node; while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertion(e) || ts.isNonNullExpression(e)) { @@ -2960,11 +2986,11 @@ export class Lowerer { return symbol !== null && optionalSymbols.has(symbol); } if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) { - const symbol = symbolOf(e.expression); + const symbol = callableSymbolOf(e.expression); return symbol !== null && optionalReturns.has(symbol); } if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) { - const symbol = symbolOf(e.expression.name); + const symbol = callableSymbolOf(e.expression); return symbol !== null && optionalReturns.has(symbol); } if (ts.isConditionalExpression(e)) { @@ -3362,38 +3388,40 @@ export class Lowerer { ts.isCallExpression(node) && (ts.isIdentifier(node.expression) || ts.isPropertyAccessExpression(node.expression)) ) { - const calleeNode = ts.isIdentifier(node.expression) ? node.expression : node.expression.name; - const symbol = symbolOf(calleeNode); + const symbol = callableSymbolOf(node.expression); if (!symbol) return; const sig = signatureBySymbol.get(symbol); - if (!sig) { - const decl = this.checker.valueDeclarationOf(symbol); - if (decl && ts.isVariableDeclaration(decl) && decl.initializer && ts.isIdentifier(decl.initializer)) { - const source = symbolOf(decl.initializer); - const sourceSig = source ? signatureBySymbol.get(source) : undefined; - if (sourceSig) { - node.arguments.forEach((arg, i) => { - if (ts.isSpreadElement(arg) || !mayBeOptional(arg) || !sourceSig.params[i]) return; - const before = sourceSig.params[i]!.type; - sourceSig.params[i]!.type = this.runtimeOptionalType(before); - if (!typeEquals(before, sourceSig.params[i]!.type)) { - changed = true; - const sourceFn = functionDeclBySymbol.get(source!); - const parameter = sourceFn?.parameters[i]; - if (parameter) { - for (const bound of boundIdentifiersOf(parameter.name)) { - const boundSymbol = symbolOf(bound); - if (boundSymbol && !optionalSymbols.has(boundSymbol)) { - optionalSymbols.add(boundSymbol); - changed = true; - } - } + if (!sig) return; + // JSDoc describes the usual value, not a runtime arity check. + // A short JavaScript call still passes undefined. Widen only the + // observed slots before bodies lower so both caller and callee + // agree on that value's representation. + const callee = functionDeclBySymbol.get(symbol); + if (callee && isJsSourceFile(callee.getSourceFile()) && !node.arguments.some(ts.isSpreadElement)) { + for (let i = 0; i < sig.params.length; i++) { + const arg = node.arguments[i]; + if (arg && (this.typeOf(arg).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0) continue; + const family = familyBySymbol.get(symbol); + if (family) optionalMethodFamilies.add(family); + for (const target of familyBySymbol.get(symbol) ?? [symbol]) { + const shape = signatureBySymbol.get(target)?.params[i]; + if (!shape || shape.mode !== "required") continue; + const widened = addUndefined(shape.type); + if (typeEquals(shape.type, widened)) continue; + shape.type = widened; + const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + if (parameter) { + for (const bound of boundIdentifiersOf(parameter.name)) { + const boundSymbol = symbolOf(bound); + if (boundSymbol) { + optionalSymbols.add(boundSymbol); + if (ts.isIdentifier(parameter.name)) this.runtimeOptionalBindingTypes.set(boundSymbol, shape.type); } } - }); + } + changed = true; } } - return; } node.arguments.forEach((arg, i) => { const callbackSlot = sig.params[i]?.type; @@ -3406,6 +3434,7 @@ export class Lowerer { } if (ts.isSpreadElement(arg) || !mayBeOptional(arg) || !sig.params[i]) return; const set = optionalParams.get(symbol) ?? new Set(); + sig.params[i]!.type = addUndefined(sig.params[i]!.type); const before = set.size; set.add(i); optionalParams.set(symbol, set); @@ -3449,6 +3478,17 @@ export class Lowerer { changed = false; for (const sf of sourceFiles) if (scanFile(sf)) changed = true; for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true; + // Every implementation of a virtual slot must retain the same ABI, + // including when just one body forwards an omitted parameter. + for (const family of optionalMethodFamilies) { + if (!family.some((symbol) => optionalReturns.has(symbol))) continue; + for (const symbol of family) { + if (!optionalReturns.has(symbol)) { + optionalReturns.add(symbol); + changed = true; + } + } + } } for (const [symbol, sig] of signatureBySymbol) { const params = optionalParams.get(symbol); @@ -7814,6 +7854,20 @@ export class Lowerer { if (helper) { e = { kind: "call", callee: helper, args: [e], type: expected, loc: e.loc }; } + // Shipped JavaScript can select overloads using runtime brand data + // that its JSDoc union does not narrow. Preserve the numeric/string/ + // boolean destination check, just as for an unknown-typed argument; + // never reinterpret an object payload as a scalar union arm. + if (e.type.kind === "union" && !typeEquals(e.type, expected) && isJsSourceFile(node.getSourceFile()) && + this.dynConvertible(e.type)) { + const sourceArms = this.unions.get(e.type.unionId)?.arms ?? []; + const targetArms = this.unions.get(expected.unionId)?.arms ?? []; + if (sourceArms.some(isDynTypedRefType) && targetArms.length > 0 && + targetArms.every((arm) => isUnitType(arm) || arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool") && + targetArms.some((arm) => !isUnitType(arm) && sourceArms.some((source) => typeEquals(source, arm)))) { + e = this.coerceToExpected(this.coerceToExpected(e, DYN), expected); + } + } } // A JS FUNC value outside the island marshal set flowing into a // jsval slot (`withPlugins(getSupportInfoWithoutPlugins, 0)` — a diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index 6934c0af1..10da3cf8d 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -103,7 +103,7 @@ export function planNpmStaticReexports( entry: ts.SourceFile, files: readonly ts.SourceFile[], extraRoots: readonly string[], - resolveEdge: (from: ts.SourceFile, spec: string) => ts.SourceFile | null, + resolveEdge: (from: ts.SourceFile, spec: string, resolutionKind?: "import" | "require") => ts.SourceFile | null, ): ts.SourceFile[] { const available = new Set(files); const demanded = new Map(); @@ -186,7 +186,7 @@ export function planNpmStaticReexports( if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined; if (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") { - request(resolveEdge(sf, arg.text), null); + request(resolveEdge(sf, arg.text, node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require"), null); } return undefined; }); diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts index 19d985da2..6fa6f38fd 100644 --- a/packages/compiler/src/frontend/npm-static.ts +++ b/packages/compiler/src/frontend/npm-static.ts @@ -195,18 +195,9 @@ function shadowTargetOf(path: string): { pkg: string; viaTypes: boolean } | null * and every "types"/"typings" condition key inside "exports" * (recursively; condition objects nest) — so resolution lands on * runtime JS; - * 2. CJS-first: a condition object carrying BOTH "import" and "require" - * has its "import" value REWRITTEN to the "require" target. The - * import-side of a wrapper-style dual (commander's esm.mjs) is pure - * name plumbing over the CJS implementation — `export const {…} = - * cjsDefault`, a re-export shape with no static lowering — while the - * CJS side engages the compiler's richest machinery (the CJS lexer - * link check, export-identity analysis, require discipline). - * Behavior is identical for wrapper duals (both sides evaluate the - * same CJS module); genuinely dual-BEHAVIOR packages are outside the - * pilot's supported surface — the differential contract gates every - * opted-in package. Import-only (pure ESM) packages are untouched - * and compile as native ES modules. */ + * 2. preserve distinct import and require targets. Static compilation + * must execute the same module Node selects, including packages that + * ship separate ESM and CommonJS builds. */ export function npmStaticTransformPkgJson(pkg: Record): void { delete pkg["types"]; delete pkg["typings"]; @@ -247,9 +238,6 @@ export function npmStaticTransformPkgJson(pkg: Record): void { } } } - if (obj["import"] !== undefined && obj["require"] !== undefined) { - obj["import"] = obj["require"]; - } for (const value of Object.values(obj)) transform(value); }; if (pkg["exports"] !== undefined) transform(pkg["exports"]); diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index cd22eb101..9a78f022c 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -487,7 +487,7 @@ function createRequireProgramRoots7(program: ts.Program): string[] { if (canonicalBuiltinModule(spec) !== null) return "skip"; let target = resolveProjectModule(sf.fileName, spec); if (target === null && !spec.startsWith("#")) { - const npm = resolveNpmImport7(sf.fileName, spec); + const npm = resolveNpmImport7(sf.fileName, spec, "require"); if (npm !== null && isNpmStaticPackage(npm.packageName) && isJsSourceFileName(npm.typesFile)) { target = npm.typesFile; } @@ -2065,6 +2065,7 @@ function resolveImport7(program: ts.Program, from: ts.SourceFile, specifier: str function resolveNpmImport7( fromFileName: string, specifier: string, + resolutionKind: "import" | "require" = "import", ): { packageName: string; version?: string; typesFile: string } | null { if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) { return null; @@ -2073,7 +2074,7 @@ function resolveNpmImport7( // its attested source compiles as program modules (resolveProjectModule // answers the entry), so no island embed and no .d.ts type surface. if (provenanceEntryFor(specifier) !== null) return null; - const resolved = resolveBareModule(fromFileName, specifier); + const resolved = resolveBareModule(fromFileName, specifier, undefined, resolutionKind); if (!resolved) return null; if (!isNodeModulesPath(resolved.typesFile)) { // A workspace-linked package (the node_modules entry is a symlink into @@ -2426,7 +2427,7 @@ function preflight7(load: LoadResult): { entry, programFiles, [...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getSourceFiles())], - (sf, spec) => resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec), + (sf, spec, resolutionKind) => resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec, resolutionKind), ) : programFiles; program.getTypeChecker().prefetchSourceFileStructures(userFiles); @@ -2889,7 +2890,7 @@ function preflight7(load: LoadResult): { // form above (bundle dists require their workspace siblings — // the same resolution, the same offender discipline on a // miss). - const npmReq = !req.spec.startsWith("#") ? resolveNpmImport7(sf.fileName, req.spec) : null; + const npmReq = !req.spec.startsWith("#") ? resolveNpmImport7(sf.fileName, req.spec, "require") : null; if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { dep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); if (dep === null) continue; // offender recorded — the fallback loop reloads @@ -2978,7 +2979,7 @@ function preflight7(load: LoadResult): { if (!isRelativeSpecifier(spec)) { // --npm-static: opted-in packages ride the program-module edge // (the statement-level require branch above). - const npmReq = !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec) : null; + const npmReq = !spec.startsWith("#") ? resolveNpmImport7(sf.fileName, spec, "require") : null; if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); if (nDep !== null) deps.push({ dep: nDep }); @@ -3159,7 +3160,7 @@ function cjsNamedImportLinkCheck( // Reexport targets union in only when they resolve to CommonJS program // files (Node's cjsPreparseModuleExports rule). const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { - const dep = resolveEdge(from, spec); + const dep = resolveImport7(program, from, spec) ?? npmStaticDepSf7(program, from, spec, "require"); return dep !== null && isCjsJsFile7(dep, program) ? dep : null; }; const visible = (dep: ts.SourceFile, name: string): boolean => @@ -3725,10 +3726,10 @@ export function orderedImportsOf( * as a module edge), else null. No offender reporting here — preflight * already classified the import; this is the lookup the module-order and * lowering paths share. */ -export function npmStaticDepSf7(program: ts.Program, sf: ts.SourceFile, spec: string): ts.SourceFile | null { +export function npmStaticDepSf7(program: ts.Program, sf: ts.SourceFile, spec: string, resolutionKind: "import" | "require" = "import"): ts.SourceFile | null { if (!npmStaticActive() || isRelativeSpecifier(spec)) return null; if (spec.startsWith("node:") || spec.startsWith("#")) return null; - const npm = resolveNpmImport7(sf.fileName, spec); + const npm = resolveNpmImport7(sf.fileName, spec, resolutionKind); if (npm === null || !isNpmStaticPackage(npm.packageName)) return null; if (!isJsSourceFileName(npm.typesFile)) return null; return program.getSourceFile(npm.typesFile) ?? null; diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index d8dc74f8e..ab9c2da96 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -401,6 +401,7 @@ const EXPORT_CONDITIONS = new Set(["types", "import", "default"]); * runtime JS is the compile target). Mirrors the types-stripped * package.json the tsgo host serves for the same package. */ const JS_ONLY_CONDITIONS = new Set(["import", "default"]); +const JS_REQUIRE_CONDITIONS = new Set(["require", "default"]); /** Node's ESM runtime conditions. Source-only workspace detection follows * the executable branch rather than the checker-only "types" branch. */ @@ -809,6 +810,8 @@ export function resolveBareModule( * "runtime-js" and "runtime-source" probe Node's import-condition entry * by executable file kind; default follows the active set. */ mode?: "js-only" | "types-only" | "runtime-js" | "runtime-source", + /** Select the actual module edge when compiling an opted-in package. */ + resolutionKind: "import" | "require" = "import", ): BareResolution | null { const pkgName = packageNameOfSpecifier(specifier); const rest = specifier.slice(pkgName.length).replace(/^\//, ""); @@ -821,7 +824,7 @@ export function resolveBareModule( const conditions = runtimeImport ? RUNTIME_IMPORT_CONDITIONS : npmStatic - ? JS_ONLY_CONDITIONS + ? resolutionKind === "require" ? JS_REQUIRE_CONDITIONS : JS_ONLY_CONDITIONS : EXPORT_CONDITIONS; const runtimeOnly = runtimeImport || npmStatic; @@ -850,7 +853,7 @@ export function resolveBareModule( }; const rawPkg = pkgJsonOf(nmPkgDir); // The opted-in exports lookup runs over the SAME transformed document - // the tsgo host serves (types stripped, import:=require) — the two + // the tsgo host serves (types stripped, runtime conditions preserved) — the two // resolvers must answer one file. Cloned per lookup; never cached, so // the raw cache stays clean for flagless compiles. const pkg = diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index f4ca98ba8..3b4320eca 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1146,6 +1146,9 @@ export interface IrClassDef { jsName?: string; /** JavaScript constructor arity, ending before the first default or rest parameter. */ jsLength?: number; + /** Zero-argument native helper returning the shared prototype data object. + * Used when materializing an instance's own-property view. */ + prototypeDataHelper?: string; /** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC * helpers, and vtable live in the runtime (ScrError / scr_error_*), so * backends emit no definitions for it — only the preorder-interval diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index f8c5d1876..f7df55bb1 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -39,6 +39,23 @@ function localClassModule(): IrModule { return mod; } +test("class prototype data helpers retain their ABI after serialization", () => { + const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, []); + mod.classes = [{ name: "Vector", fields: [], prototypeDataHelper: "%prototype.Vector", loc }]; + mod.functions.push({ name: "%prototype.Vector", params: [], locals: [], returnType: DYN, + body: [{ kind: "return", value: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc }], loc }); + expect(validateModule(deserializeModule(serializeModule(mod)))).toEqual([]); + for (const variant of ["missing", "params", "return", "captures"]) { + const bad = structuredClone(mod); + const helper = bad.functions[1]!; + if (variant === "missing") bad.functions.pop(); + if (variant === "params") helper.params.push({ localId: "p", name: "p", type: DYN }); + if (variant === "return") helper.returnType = F64; + if (variant === "captures") helper.captures = []; + expect(validateModule(bad).some((error) => error.message.includes("prototype data helper"))).toBe(true); + } +}); + test("local classes retain serialized capture slots and fresh identity", () => { const mod = localClassModule(); expect(validateModule(mod)).toEqual([]); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 66fb454d1..77e577ab2 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1691,6 +1691,12 @@ export function validateModule(mod: IrModule): IrValidationError[] { if (cls.jsLength !== undefined && (!Number.isSafeInteger(cls.jsLength) || cls.jsLength < 0)) { errors.push({ message: `class ${cls.name}: invalid constructor length`, loc: cls.loc }); } + if (cls.prototypeDataHelper !== undefined) { + const helper = functionsByName.get(cls.prototypeDataHelper); + if (!helper || helper.params.length !== 0 || helper.returnType.kind !== "dyn" || helper.captures !== undefined || helper.classCaptures !== undefined) { + errors.push({ message: `class ${cls.name}: prototype data helper must be a noncapturing () => dyn function`, loc: cls.loc }); + } + } const seen = new Set(); for (const f of cls.fields) { if (seen.has(f.name)) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 6deef5d75..6db33fc2f 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5452,6 +5452,13 @@ static bool scr_dyn_canonical_own_index(const ScrStr *key, size_t length) { } bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { + if (v->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(v); + if (!view) return false; + bool result = scr_dyn_has_own(view, key); + scr_dyn_release(view); + return result; + } if (v->kind == SCR_DYN_PROXY) { ScrDyn *desc = scr_dyn_own_descriptor(v, key); bool has = desc && desc->kind != SCR_DYN_UNDEF; @@ -5490,8 +5497,7 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { scr_dyn_release(desc); return has; } - if (v->kind == SCR_DYN_HANDLE || - v->kind == SCR_DYN_TYPED_REF) { + if (v->kind == SCR_DYN_HANDLE) { const char *m = "Own-property checks on this checked-dynamic kind are not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, m, strlen(m)); return false; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b3ddb48d..5541d351d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: js-yaml: specifier: 4.3.0 version: 4.3.0 + three: + specifier: 0.186.1 + version: 0.186.1 tsx: specifier: ^4.19.0 version: 4.23.0 @@ -2372,6 +2375,9 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + three@0.186.1: + resolution: {integrity: sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -4730,6 +4736,8 @@ snapshots: transitivePeerDependencies: - react-native-b4a + three@0.186.1: {} + throttleit@2.1.0: {} time-span@4.0.0: diff --git a/tests/corpus/class-brand-overload.js b/tests/corpus/class-brand-overload.js new file mode 100644 index 000000000..29ac47c3d --- /dev/null +++ b/tests/corpus/class-brand-overload.js @@ -0,0 +1,15 @@ +class Point { + static { Point.prototype.isPoint = true; } + constructor(x = 0) { this.x = x; } +} +/** @param {number} [value] */ +function scalar(value) { return value === undefined ? 'missing' : value.toString(); } +/** @param {number|Point} value */ +function coordinate(value) { + if (value.isPoint) return scalar(value.x); + return scalar(value); +} +console.log(coordinate(7), coordinate(new Point(9)), coordinate(0)); +/** @param {number|Point|undefined} value */ +function optionalBrand(value) { return value?.isPoint; } +console.log(optionalBrand(undefined), optionalBrand(2), optionalBrand(new Point(3))); diff --git a/tests/corpus/class-prototype-data.js b/tests/corpus/class-prototype-data.js new file mode 100644 index 000000000..1acf46599 --- /dev/null +++ b/tests/corpus/class-prototype-data.js @@ -0,0 +1,38 @@ +class Vector { + static { Vector.prototype.isVector = true; } + constructor(x = 0) { this.x = x; } +} +class ColoredVector extends Vector { + static { ColoredVector.prototype.color = 'red'; } +} +const first = new Vector(3); +const second = new Vector(4); +const child = new ColoredVector(5); +console.log(first.x, first.isVector, second.isVector, child.isVector, child.color); +console.log(Object.hasOwn(first, 'isVector'), Object.keys(first).join(',')); +Vector.prototype.isVector = false; +console.log(first.isVector, second.isVector, child.isVector); +first.isVector = true; +console.log(first.isVector, second.isVector, Object.hasOwn(first, 'isVector')); +ColoredVector.prototype.isVector = true; +console.log(child.isVector, second.isVector); +console.log(Vector.prototype.isVector, ColoredVector.prototype.isVector); +function read(value) { return value.isVector; } +console.log(read(first), read(second), read(child)); +/** @param {unknown} value */ +function snapshot(value) { return JSON.stringify(value); } +console.log(snapshot(first), snapshot(second), snapshot(child)); +/** @param {unknown} value */ +function inherited(value) { return 'isVector' in value; } +console.log(inherited(first), inherited(second), inherited(child)); +// Assignment is discovered after this instance's checked view is created. +class LateVector { constructor() { this.x = 8; } } +class LateChild extends LateVector {} +const late = new LateChild(); +console.log(read(late)); +LateVector.prototype.isVector = 'late'; +console.log(read(late), late.isVector, Object.hasOwn(late, 'isVector')); +LateChild.prototype.isVector = undefined; +console.log(read(late), late.isVector, inherited(late)); +late.isVector = 'own'; +console.log(read(late), snapshot(late), Object.keys(late).join(',')); diff --git a/tests/corpus/js-omitted-typed-arguments.js b/tests/corpus/js-omitted-typed-arguments.js new file mode 100644 index 000000000..2257f589a --- /dev/null +++ b/tests/corpus/js-omitted-typed-arguments.js @@ -0,0 +1,33 @@ +/** @param {number} value */ +function scalar(value) { return value; } +class Matrix { + /** @param {number} x @param {number} y @param {number} z */ + translate(x, y, z) { + console.log(x, y, z, typeof y, y === undefined); + return y; + } +} +console.log(scalar(), scalar(3)); +const matrix = new Matrix(); +console.log(matrix.translate(2)); +console.log(matrix.translate(2, 4, 6)); +const alias = scalar; +console.log(alias(), alias(5)); +/** @param {number} value */ +function increment(value) { return value + 1; } +console.log(increment(), increment(2)); +console.log(increment(undefined), increment(void console.log('argument effect'))); +/** @param {string} value */ +function echo(value) { return value; } +const echoAlias = echo; +console.log(echoAlias(), echoAlias('hello')); +class DerivedMatrix extends Matrix { + /** @param {number} x @param {number} y @param {number} z */ + translate(x, y, z) { + console.log('derived', x, y, z); + return y; + } +} +/** @param {Matrix} value */ +function invoke(value) { console.log(value.translate(1)); } +invoke(new DerivedMatrix()); diff --git a/tests/corpus/math-function-values.js b/tests/corpus/math-function-values.js new file mode 100644 index 000000000..5662808c8 --- /dev/null +++ b/tests/corpus/math-function-values.js @@ -0,0 +1,13 @@ +const sin = Math.sin; +const cos = Math.cos; +const atan2 = Math.atan2; +const sqrt = Math.sqrt; +const imul = Math.imul; +const abs = Math.abs; +console.log(sin(0), cos(0), sqrt(25), imul(3, 7), abs(-4)); +console.log(atan2(0, 1), Object.is(sin(-0), -0), Object.is(abs(-0), 0)); +console.log([0, 1, 4, 9].map(sqrt).join(',')); +function evaluate(fn, value) { return fn(value); } +console.log(evaluate(cos, 0), evaluate(sqrt, 81)); +console.log(sin === Math.sin, sin === cos); +console.log(Number.isNaN(sqrt(-1)), sqrt(Infinity)); diff --git a/tests/diagnostics/class-prototype-reflection.ts b/tests/diagnostics/class-prototype-reflection.ts new file mode 100644 index 000000000..a36c1793d --- /dev/null +++ b/tests/diagnostics/class-prototype-reflection.ts @@ -0,0 +1,10 @@ +class Vector { + x = 0; + length(): number { return this.x; } +} +// Prototype data is represented; compiled method descriptors are not. +const prototype = Vector.prototype; +const method = Vector.prototype.length; +Vector.prototype.length = () => 1; +Object.getOwnPropertyDescriptors(Vector.prototype); +// End of the prototype reflection diagnostic fixture. diff --git a/tests/diagnostics/stdlib-fence.ts b/tests/diagnostics/stdlib-fence.ts index fa9884d9b..37e0fb5d9 100644 --- a/tests/diagnostics/stdlib-fence.ts +++ b/tests/diagnostics/stdlib-fence.ts @@ -34,7 +34,7 @@ const st = new Set(new Set([1, 2])); // string and array seeds lower; Set-valued const agg = new AggregateError([]); const stack = new Error("boom").stack; // Unlowered members on lowered containers. -const sinhMethod = Math.sinh; +const sinhMethod = Math.sinh; // fixed-arity Math function values now lower // (n-ary Math.min/max LOWER now — the variadic battery lives in the // corpus; the mixed spread/positional list is the form that stays fenced.) const clamped = Math.min(1, ...[2, 3]); diff --git a/tests/diagnostics/stdlib.ts b/tests/diagnostics/stdlib.ts index 2880c6bf3..d88b5da2b 100644 --- a/tests/diagnostics/stdlib.ts +++ b/tests/diagnostics/stdlib.ts @@ -1,6 +1,6 @@ // tsc-clean misuses of the standard-library surface: each line below is // valid TypeScript against the ambient declarations but outside the -// supported lowering (most library, island-backed, Math, string, and +// supported lowering (most library, island-backed, string, and // number functions have no value form; descriptor-backed Node functions // are the explicit exception; `process` itself is not a first-class value). import { mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs"; @@ -9,7 +9,7 @@ const read = readFileSync; console.log(read); const cwd = process.cwd; const p = process; const env = process.env; -const flo = Math.floor; +const flo = Math.floor; // fixed-arity Math functions have native value forms const upper = "abc".toUpperCase; const fix = (1.5).toFixed; const pf = parseFloat; diff --git a/tests/fixtures/three/math.mjs b/tests/fixtures/three/math.mjs new file mode 100644 index 000000000..9dfe002d1 --- /dev/null +++ b/tests/fixtures/three/math.mjs @@ -0,0 +1,31 @@ +import { Vector2 } from 'three/src/math/Vector2.js'; +import { Vector3 } from 'three/src/math/Vector3.js'; +import { Quaternion } from 'three/src/math/Quaternion.js'; +import { Matrix4 } from 'three/src/math/Matrix4.js'; +import { setQuaternionFromProperEuler } from 'three/src/math/MathUtils.js'; + +const point = new Vector3(1, 2, 3); +const translation = new Matrix4().makeTranslation(4, 5, 6); +point.applyMatrix4(translation); +console.log('translation', point.x, point.y, point.z, translation.isMatrix4); +const fromVector = new Matrix4().makeTranslation(new Vector3(2, 3, 4)); +point.applyMatrix4(fromVector); +console.log('vector overload', point.x, point.y, point.z); +point.applyMatrix4(fromVector.clone().invert()); +console.log('inverse', point.x, point.y, point.z); + +const quaternion = new Quaternion(); +setQuaternionFromProperEuler(quaternion, 0, 0, 0, 'XYX'); +console.log('quaternion', quaternion.x, quaternion.y, quaternion.z, quaternion.w); +const composed = new Matrix4().compose(new Vector3(3, 4, 5), quaternion, new Vector3(2, 3, 4)); +const transformed = new Vector3(1, 2, 3).applyMatrix4(composed); +console.log('compose', transformed.x, transformed.y, transformed.z); + +const planar = new Vector2(3, 4); +console.log('vector2', planar.length(), planar.isVector2); +planar.add(new Vector2(1, 2)).multiplyScalar(2); +console.log('arithmetic', planar.x, planar.y); +const normal = new Vector3(1, 0, 0).cross(new Vector3(0, 1, 0)); +console.log('cross', normal.x, normal.y, normal.z, normal.isVector3); +console.log('dot', normal.dot(new Vector3(2, 3, 4))); +try { planar.getComponent(3); } catch (error) { console.log(error.name, error.message); } diff --git a/tests/harness/__snapshots__/class-prototype-reflection.ts.txt b/tests/harness/__snapshots__/class-prototype-reflection.ts.txt new file mode 100644 index 000000000..572c684d1 --- /dev/null +++ b/tests/harness/__snapshots__/class-prototype-reflection.ts.txt @@ -0,0 +1,27 @@ +class-prototype-reflection.ts:6:19 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet + + 5 | // Prototype data is represented; compiled method descriptors are not. + 6 | const prototype = Vector.prototype; + | ^~~~~~~~~~~~~~~~ + 7 | const method = Vector.prototype.length; + +class-prototype-reflection.ts:7:16 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet + + 6 | const prototype = Vector.prototype; + 7 | const method = Vector.prototype.length; + | ^~~~~~~~~~~~~~~~ + 8 | Vector.prototype.length = () => 1; + +class-prototype-reflection.ts:8:1 - error SC1090: assignment to non-variables are not supported yet + + 7 | const method = Vector.prototype.length; + 8 | Vector.prototype.length = () => 1; + | ^~~~~~~~~~~~~~~~~~~~~~~ + 9 | Object.getOwnPropertyDescriptors(Vector.prototype); + +class-prototype-reflection.ts:9:34 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet + + 8 | Vector.prototype.length = () => 1; + 9 | Object.getOwnPropertyDescriptors(Vector.prototype); + | ^~~~~~~~~~~~~~~~ + 10 | // End of the prototype reflection diagnostic fixture. \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index c9307bf65..dc4351dce 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -127,13 +127,6 @@ stdlib-fence.ts:35:15 - error SC2020: 'Error.stack' is part of the standard libr hint: stack traces are not captured (frames would need runtime bookkeeping); name, message, and toString() are available -stdlib-fence.ts:37:20 - error SC1090: Math methods as values (call 'sinh' directly) are not supported yet - - 36 | // Unlowered members on lowered containers. - 37 | const sinhMethod = Math.sinh; - | ^~~~~~~~~ - 38 | // (n-ary Math.min/max LOWER now — the variadic battery lives in the - stdlib-fence.ts:40:17 - error SC2020: 'Math.min' is part of the standard library types but has no scriptc lowering yet 39 | // corpus; the mixed spread/positional list is the form that stays fenced.) diff --git a/tests/harness/__snapshots__/stdlib.ts.txt b/tests/harness/__snapshots__/stdlib.ts.txt index e6be178cc..dbe79859a 100644 --- a/tests/harness/__snapshots__/stdlib.ts.txt +++ b/tests/harness/__snapshots__/stdlib.ts.txt @@ -12,16 +12,9 @@ stdlib.ts:9:13 - error SC1090: process methods as values (call 'cwd' directly) a | ^~~~~~~~~~~ 10 | const p = process; -stdlib.ts:12:13 - error SC1090: Math methods as values (call 'floor' directly) are not supported yet - - 11 | const env = process.env; - 12 | const flo = Math.floor; - | ^~~~~~~~~~ - 13 | const upper = "abc".toUpperCase; - stdlib.ts:13:15 - error SC1090: string methods as values (call 'toUpperCase' directly) are not supported yet - 12 | const flo = Math.floor; + 12 | const flo = Math.floor; // fixed-arity Math functions have native value forms 13 | const upper = "abc".toUpperCase; | ^~~~~~~~~~~~~~~~~ 14 | const fix = (1.5).toFixed; diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index fa333b90e..7edbeea21 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -99,6 +99,39 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise } describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { + test("conditional exports keep distinct import and require behavior", async () => { + const dir = mkdtempSync("/tmp/scriptc-conditional-exports-"); + try { + const pkg = join(dir, "node_modules", "dual"); + mkdirSync(pkg, { recursive: true }); + writeFileSync(join(pkg, "package.json"), JSON.stringify({ + name: "dual", type: "module", + exports: { ".": { types: "./index.d.ts", node: { import: "./esm.js", require: "./cjs.cjs" }, default: "./browser.js" } }, + })); + writeFileSync(join(pkg, "index.d.ts"), "export declare const value: string;"); + writeFileSync(join(pkg, "esm.js"), 'console.log("esm init"); export const value = "import";'); + writeFileSync(join(pkg, "cjs.cjs"), 'console.log("cjs init"); exports.value = "require";'); + writeFileSync(join(pkg, "browser.js"), 'export const value = "browser";'); + for (const [name, source] of [ + ["main.mjs", 'import { value } from "dual"; console.log(value);'], + ["main.cjs", 'const { value } = require("dual"); console.log(value);'], + ["mixed.mjs", 'import { value } from "dual"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const cjs = require("dual"); console.log(value, cjs.value);'], + ] as const) { + const entry = join(dir, name); + writeFileSync(entry, source); + const result = await compile(entry, { backend: "llvm", dynamic: false, npmStatic: ["dual"], sanitize, + outDir: join(dir, name + "-out"), outPath: join(dir, name + "-program") }); + if (!result.ok) throw new Error(name + ": " + result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const [reference, native] = await Promise.all([runBinary(process.execPath, [entry]), runBinary(result.binaryPath, [])]); + expect(native.stdout).toEqual(reference.stdout); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toEqual(reference.exitCode); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + test.each(["llvm"] as const)("renderer values and callbacks compile from shipped JavaScript (%s)", async (backend) => { const dir = mkdtempSync(join(tmpdir(), "scriptc-renderer-values-")); try { diff --git a/tests/harness/three.test.ts b/tests/harness/three.test.ts new file mode 100644 index 000000000..c6badd955 --- /dev/null +++ b/tests/harness/three.test.ts @@ -0,0 +1,57 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const entry = join(import.meta.dirname, "../fixtures/three/math.mjs"); +const hasZig = spawnSync("zig", ["version"], { stdio: "ignore" }).status === 0; +const wasiRunner = ` +const {readFileSync} = require('node:fs'); +const {WASI} = require('node:wasi'); +const path = process.argv[1]; +const wasi = new WASI({version:'preview1', args:[path], returnOnExit:true}); +const module = new WebAssembly.Module(readFileSync(path)); +const instance = new WebAssembly.Instance(module, wasi.getImportObject()); +process.exitCode = wasi.start(instance); +`; + +function run(command: string, args: string[]) { + const result = spawnSync(command, args, { encoding: "utf8", timeout: 30_000 }); + if (result.error) throw result.error; + return { stdout: result.stdout, stderr: result.stderr, status: result.status, signal: result.signal }; +} + +test.for(["native", "wasm32-wasi"])("published three.js math runs statically through LLVM on %s", async (target, context) => { + if (target === "wasm32-wasi" && !hasZig) context.skip(); + const dir = await mkdtemp("/tmp/scriptc-three-"); + const previousTarget = process.env["SCRIPTC_TARGET"]; + const previousCc = process.env["SCRIPTC_CC"]; + const wasm = target === "wasm32-wasi"; + try { + if (wasm) { + process.env["SCRIPTC_TARGET"] = target; + process.env["SCRIPTC_CC"] = "zigcc"; + } + const result = await compile(entry, { + outDir: dir, outPath: join(dir, wasm ? "math.wasm" : "math"), + backend: "llvm", dynamic: false, npmStatic: ["three"], + sanitize: !wasm && process.env["SCRIPTC_SAN"] === "1", + }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + expect(result.backend).toBe("llvm"); + if (wasm) expect([...(await readFile(result.binaryPath)).subarray(0, 4)]).toEqual([0, 97, 115, 109]); + const reference = run(process.execPath, ["--no-warnings", entry]); + expect(reference.status).toBe(0); + const actual = wasm + ? run(process.execPath, ["--no-warnings", "-e", wasiRunner, result.binaryPath]) + : run(result.binaryPath, []); + expect(actual).toEqual(reference); + } finally { + if (previousTarget === undefined) delete process.env["SCRIPTC_TARGET"]; + else process.env["SCRIPTC_TARGET"] = previousTarget; + if (previousCc === undefined) delete process.env["SCRIPTC_CC"]; + else process.env["SCRIPTC_CC"] = previousCc; + await rm(dir, { recursive: true, force: true }); + } +});