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:
Chris Tate
2026-09-29 20:58:04 -05:00
committed by GitHub
parent 3f66c8e20b
commit d3dfdade8e
32 changed files with 576 additions and 94 deletions
+1
View File
@@ -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;
});
+3 -15
View File
@@ -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"]);
+9 -8
View File
@@ -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;
+5 -2
View File
@@ -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 =
+3
View File
@@ -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
+17
View File
@@ -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([]);
+6
View File
@@ -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)) {
+8 -2
View File
@@ -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;
+8
View File
@@ -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:
+15
View File
@@ -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)));
+38
View File
@@ -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());
+13
View File
@@ -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.
+1 -1
View File
@@ -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]);
+2 -2
View File
@@ -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;
+31
View File
@@ -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.)
+1 -8
View File
@@ -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;
+33
View File
@@ -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 {
+57
View File
@@ -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 });
}
});