mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<IrExpr, { kind: "dynInvoke" }>;
|
||||
interface Dispatch {
|
||||
@@ -31,6 +32,7 @@ export class ClassDynamicDispatch {
|
||||
private readonly computed = new Map<string, Omit<PropertyDispatch, "name"> & { keyLocal: string; branchIndex: number }>();
|
||||
private propertyBag: IrFunction | null = null;
|
||||
private readonly bagClasses = new Set<string>();
|
||||
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
|
||||
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<IrStmt, { kind: "fieldSet" }> = {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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) };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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<ts.Symbol, RuntimeSig>();
|
||||
const methodFamilies = new Map<string, ts.Symbol[]>();
|
||||
const familyBySymbol = new Map<ts.Symbol, ts.Symbol[]>();
|
||||
const optionalMethodFamilies = new Set<ts.Symbol[]>();
|
||||
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<ts.Symbol>();
|
||||
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<number>();
|
||||
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
|
||||
|
||||
@@ -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<ts.SourceFile, Demand>();
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>): void {
|
||||
delete pkg["types"];
|
||||
delete pkg["typings"];
|
||||
@@ -247,9 +238,6 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): 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"]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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<string>();
|
||||
for (const f of cls.fields) {
|
||||
if (seen.has(f.name)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+8
@@ -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:
|
||||
|
||||
@@ -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)));
|
||||
@@ -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(','));
|
||||
@@ -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());
|
||||
@@ -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));
|
||||
@@ -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.
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+31
@@ -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); }
|
||||
@@ -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.
|
||||
@@ -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.)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user