mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
fix: preserve compiler coverage after syncing main
- Restore native self-compilation and deferred class diagnostics. - Preserve numeric spread calls on JavaScript array results. - Record new frontend baselines and repair optional spatial test coverage.
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"effect": "3.22.2",
|
||||
"eslint": "^9.20.0",
|
||||
"js-yaml": "4.3.0",
|
||||
"rollup": "4.62.2",
|
||||
"three": "0.186.1",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -184,9 +184,12 @@ export interface GenericFnInfo {
|
||||
* Instance methods take `this` (object:<declarer>) as param 0 and lower
|
||||
* under the declarer's instantiation bindings (generic-class receivers)
|
||||
* MERGED with the method instantiation's own; statics lower as plain
|
||||
* module functions with the static-method this/super fence. Absent for
|
||||
* module functions with an exact receiver or the this/super fence. Absent for
|
||||
* top-level functions and object-literal methods. */
|
||||
member?: { cls: ClassInfo; kind: "method" | "static" };
|
||||
/** Direct static calls specialize lexical this for the proven receiver. */
|
||||
staticReceiver?: ClassInfo;
|
||||
receiverSpecializations?: Map<string, GenericFnInfo>;
|
||||
/** Object-literal generic methods (`{ m<T>(x: T) {...} }` and generic
|
||||
* arrow/function-expression properties): lowered as plain module
|
||||
* functions — `this` inside is fenced (rejectThisInObjectMethod) and the
|
||||
@@ -1728,19 +1731,24 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
lowerer.fnStack.push(fnCtx);
|
||||
lowerer.localClassInstantiations.push({ owner: decl, name: inst.name });
|
||||
try {
|
||||
// STATIC generic methods: `this`/`super` name the RECEIVER class (a
|
||||
// dynamic value) — the lowerStaticMethod fence, applied here because
|
||||
// generic statics have no non-generic lowering pass. Arrow functions
|
||||
// are transparent (they inherit the method's `this`); this-binding
|
||||
// function forms are opaque.
|
||||
// Static specializations bind lexical this only when the caller
|
||||
// proved its exact receiver. Other instances retain the fence;
|
||||
// super remains unsupported. Arrows inherit the lexical receiver.
|
||||
if (info.member?.kind === "static" && decl.body) {
|
||||
rejectStaticThis(
|
||||
lowerer,
|
||||
decl.body,
|
||||
(keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`,
|
||||
false, info.staticReceiver !== undefined,
|
||||
);
|
||||
}
|
||||
const params: IrParam[] = [];
|
||||
const receiverInit: IrStmt[] = [];
|
||||
if (info.staticReceiver) {
|
||||
const value = lowerer.classValueRef(info.staticReceiver, decl);
|
||||
const local = lowerer.declareThis(value.type);
|
||||
receiverInit.push({ kind: "varDecl", localId: local.id, init: value, loc: locOf(decl) });
|
||||
}
|
||||
if (cls && info.member!.kind === "method") {
|
||||
// Instance methods take `this` as param 0, exactly like plain
|
||||
// `%C.method` functions (lowerClassMethodMemberInner).
|
||||
@@ -1752,7 +1760,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
// threaded — a default mentioning T resolves like any body expression.
|
||||
const declared = lowerer.declareParams(decl.parameters, inst.params);
|
||||
params.push(...declared.params);
|
||||
const body = [...declared.prologue];
|
||||
const body = [...receiverInit, ...declared.prologue];
|
||||
const bodyBlock = blockBodyOf(decl);
|
||||
if (bodyBlock) {
|
||||
body.push(...lowerer.lowerStmts(bodyBlock.statements));
|
||||
@@ -2173,7 +2181,8 @@ function runtimeOptionalHofGenericBinding(
|
||||
const broadFunction = lowerer.checker.typeToString(t) === "Function";
|
||||
const broadArray = lowerer.checkerAnyArrayType(t);
|
||||
const broadPromise = broadPromiseParam(lowerer, t);
|
||||
const classParam = classParams && lowerer.mapTypeOf(t)?.kind === "object";
|
||||
const declaredKind = classParams ? lowerer.mapTypeOf(t)?.kind : undefined;
|
||||
const classParam = classParams && (declaredKind === "object" || declaredKind === "array");
|
||||
if ((t.flags & ts.TypeFlags.Any) === 0 && !broadFunction && !broadArray && !broadPromise && !classParam) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!sym) return null;
|
||||
@@ -2233,6 +2242,8 @@ function runtimeOptionalHofGenericBinding(
|
||||
if (retTs.flags & ts.TypeFlags.Any) return null;
|
||||
if (retTs.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) return DYN;
|
||||
if (lowerer.checker.isArrayType(retTs)) {
|
||||
if (info.decl.parameters.some((param, index) => info.implicitParams?.[index] &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array")) return null;
|
||||
const elem = lowerer.checker.getTypeArguments(retTs as ts.TypeReference)[0];
|
||||
// An implicit-any parameter poisons an inferred array result to
|
||||
// any[] even when this instance's body produces one concrete
|
||||
@@ -2375,7 +2386,8 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
|
||||
// receiver type in the checked fallback. Keep existing broad JS inference.
|
||||
for (let i = 0; i < info.decl.parameters.length; i++) {
|
||||
const symbol = info.implicitParams?.[i];
|
||||
if (symbol && lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind === "object") {
|
||||
const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind;
|
||||
if (symbol && (kind === "object" || kind === "array")) {
|
||||
argTypes.set(symbol, lowerer.checker.getUnknownType());
|
||||
}
|
||||
}
|
||||
@@ -9914,7 +9926,9 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
export function lowerRecordFieldCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (lowerer.chainBlocked(call)) return null;
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type : undefined;
|
||||
const receiverType = stored?.kind === "record" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverType?.kind !== "record" && receiverType?.kind !== "union") return null;
|
||||
// A union of records can share a closure-valued field. Its normal
|
||||
// property read performs the tag dispatch before arguments evaluate.
|
||||
@@ -10896,7 +10910,8 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
// A specialized JS parameter can expose a native field whose checker
|
||||
// type still belongs to the unspecialized body (plane.normal.dot()).
|
||||
// Use the lowered receiver's representation and retain its evaluation.
|
||||
const probe = mappedReceiver?.kind !== "object" && (lowerer.implicitParamTypes !== null || ts.isNewExpression(access.expression))
|
||||
const probe = mappedReceiver?.kind !== "object" && (lowerer.implicitParamTypes !== null || ts.isNewExpression(access.expression) ||
|
||||
isJsSourceFile(call.getSourceFile()) && ts.isCallExpression(access.expression))
|
||||
? tryLowerExpression(lowerer, access.expression) : null;
|
||||
const specializedReceiver = probe?.type.kind === "object" ? probe : null;
|
||||
if (specializedReceiver) mappedReceiver = specializedReceiver.type;
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { FnCtx, Lowerer } from "./lowerer.js";
|
||||
import { BOOL, DATE_T, DYN, F64, bytesOf, type IrClassDef, type IrExpr, type IrFunction, type IrLocal, type IrParam, type IrStmt, type IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js";
|
||||
import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js";
|
||||
import { isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
|
||||
import { isGenericCallableMemberType, jsOpenObjectType, typeKey, withUnitArm } from "../type-mapper.js";
|
||||
import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js";
|
||||
import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js";
|
||||
import { lowerArrayConstructor, lowerMapSeedNew, lowerSetSeedNew } from "./lower-containers.js";
|
||||
@@ -24,10 +24,11 @@ import { builtinFenceHintOf } from "./surfaces.js";
|
||||
import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js";
|
||||
import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, fenceEarlyAliasUse, fenceEarlyNsMemberRef, nsMemberIdentOf, nsUndefRead } from "./lower-namespaces.js";
|
||||
import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixins.js";
|
||||
import { rejectStaticThis } from "./static-this.js";
|
||||
import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./static-this.js";
|
||||
import { lowerUrlNew } from "./lower-url.js";
|
||||
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
|
||||
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
||||
@@ -57,6 +58,10 @@ function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
}
|
||||
|
||||
export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression): IrType | null {
|
||||
if (expression.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const stored = lowerer.resolveThis();
|
||||
if (stored?.type.kind === "classval") return stored.type;
|
||||
}
|
||||
if (ts.isIdentifier(expression)) {
|
||||
const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression);
|
||||
if (stored?.type.kind === "classval") return stored.type;
|
||||
@@ -201,9 +206,9 @@ export interface ClassInfo {
|
||||
* never join vtables, so IrClassDef doesn't know them). `C.m(args)` is
|
||||
* a direct call; `const f = C.m` a zero-capture closure; calls through
|
||||
* class VALUES devirtualize when no strict descendant redeclares the
|
||||
* member. `this`/`super` inside fence at lowering (JS binds `this` to
|
||||
* the RECEIVER class — dynamic). Absent on builtin classes. */
|
||||
staticMethods?: Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }>;
|
||||
* member. Receiver-dependent methods specialize for exact receivers;
|
||||
* detached methods and `super` retain their fences. Absent on builtins. */
|
||||
staticMethods?: Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }>;
|
||||
/** `static { ... }` blocks, in declaration order. They are DECLARATION-TIME
|
||||
* CODE, not shape: JS runs each block once when the class statement
|
||||
* evaluates, whether or not anything ever references the class — so their
|
||||
@@ -560,6 +565,29 @@ export interface GenericClassInfo {
|
||||
return declared ? (lowerer.classes.get(irName) ?? null) : null;
|
||||
}
|
||||
|
||||
/** A dictionary lookup may be absent even when its checker type is a
|
||||
* required reference. Keep literal nullish fallbacks in inferred JS storage. */
|
||||
function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.Expression): IrType {
|
||||
let node = expression;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (node.kind === ts.SyntaxKind.NullKeyword) return withUnitArm(type, "nullT", lowerer.unions) ?? type;
|
||||
const inferred = lowerer.typeOf(node);
|
||||
for (const arm of inferred.isUnionType() ? ts.constituentTypes(inferred) : [inferred]) {
|
||||
if ((arm.flags & ts.TypeFlags.Undefined) !== 0) type = lowerer.withUndefinedArmOf(type) ?? type;
|
||||
if ((arm.flags & ts.TypeFlags.Null) !== 0) type = withUnitArm(type, "nullT", lowerer.unions) ?? type;
|
||||
}
|
||||
if (ts.isConditionalExpression(node)) {
|
||||
return jsFieldInitializerType(lowerer, jsFieldInitializerType(lowerer, type, node.whenTrue), node.whenFalse);
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) type = jsFieldInitializerType(lowerer, type, node.left);
|
||||
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) return jsFieldInitializerType(lowerer, type, node.right);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/** The undefined-armed union of a JS class property's inferred type — the
|
||||
* honest slot for a field first assigned outside the constructor's top
|
||||
* level (undefined until the write runs, Node-exact). Null when the
|
||||
@@ -571,6 +599,12 @@ export interface GenericClassInfo {
|
||||
let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null);
|
||||
if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void") return null;
|
||||
for (const declaration of lowerer.checker.declarationsOf(p)) {
|
||||
const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent;
|
||||
if (assignment && ts.isBinaryExpression(assignment) && assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
mapped = jsFieldInitializerType(lowerer, mapped, assignment.right);
|
||||
}
|
||||
}
|
||||
// An implicit JS value already carries undefined in its native box.
|
||||
if (mapped.kind === "dyn") return mapped;
|
||||
const byKey = new Map<string, IrType>();
|
||||
@@ -1269,7 +1303,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
const paramProps: NonNullable<ClassInfo["paramProps"]> = [];
|
||||
|
||||
const staticFields: ClassInfo["staticFields"] = [];
|
||||
const staticMethods = new Map<string, { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }>();
|
||||
const staticMethods: NonNullable<ClassInfo["staticMethods"]> = new Map();
|
||||
const staticBlocks: ts.ClassStaticBlockDeclaration[] = [];
|
||||
// GENERIC methods (own type parameters), instance and static: only
|
||||
// the SYNTAX is checked here — parameter/return types mention the
|
||||
@@ -2309,6 +2343,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
type = inferredEmptyCollectionFieldType(lowerer, decl, name, rhs) ?? type;
|
||||
}
|
||||
if (!type || type.kind === "void") lowerer.badType(assign, t ?? lowerer.typeOf(assign));
|
||||
type = jsFieldInitializerType(lowerer, type, rhs);
|
||||
// A JSDoc claim the BODY contradicts (`@type {Command}`
|
||||
// assigned `undefined` — the lazy-init idiom): the
|
||||
// representation follows the body — the field widens to the
|
||||
@@ -3045,7 +3080,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
* with its OWN storage, exactly JS). */
|
||||
export function findStaticOn(lowerer: Lowerer, info: ClassInfo | null, name: string):
|
||||
| { declarer: ClassInfo; field: ClassInfo["staticFields"][number]; method?: undefined }
|
||||
| { declarer: ClassInfo; method: { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration }; field?: undefined }
|
||||
| { declarer: ClassInfo; method: { params: ParamShape[]; ret: IrType; member: ts.MethodDeclaration; receiverInfo?: GenericFnInfo }; field?: undefined }
|
||||
| null {
|
||||
for (let c = info; c; c = c.base) {
|
||||
const field = c.staticFields.find((s) => s.name === name);
|
||||
@@ -3164,7 +3199,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
* Inherited writes create a new subclass property in JS, so they must not
|
||||
* update the declaring class's global. */
|
||||
export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAccessExpression): { id: string; type: IrType } | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.expression)) return null;
|
||||
if (access.questionDotToken || (!ts.isIdentifier(access.expression) && access.expression.kind !== ts.SyntaxKind.ThisKeyword)) return null;
|
||||
const info = lowerer.exactClassOfReceiver(access.expression);
|
||||
if (!info) return null;
|
||||
const found = lowerer.findStaticOn(info, access.name.text);
|
||||
@@ -3186,8 +3221,8 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* generic member rejection names the site. */
|
||||
export function lowerStaticFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.questionDotToken) return null;
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
if (!ts.isIdentifier(expr.expression) && expr.expression.kind !== ts.SyntaxKind.ThisKeyword) return null;
|
||||
const symbol = ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : null;
|
||||
const info =
|
||||
(symbol ? lowerer.classBySymbol.get(symbol) : undefined) ??
|
||||
// A require binding over `module.exports = class {…}` (the alias
|
||||
@@ -3409,6 +3444,12 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* hit the declaring class's storage exactly (the shadowing hazards of
|
||||
* general class values don't arise). Null for everything else. */
|
||||
export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null {
|
||||
// A class-valued lexical this is introduced only for an exact static
|
||||
// receiver specialization; ordinary instance this remains an object.
|
||||
if (expr.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const receiver = lowerer.resolveThis();
|
||||
return receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) ?? null : null;
|
||||
}
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
if (!symbol) return null;
|
||||
@@ -3550,6 +3591,11 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
export function lowerStaticMethodCall(lowerer: Lowerer, call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,): IrExpr | null {
|
||||
if (access.questionDotToken) return null;
|
||||
if (access.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const receiver = lowerer.resolveThis();
|
||||
const info = receiver?.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) : undefined;
|
||||
if (info) return staticCallOn(lowerer, call, access, info, false);
|
||||
}
|
||||
// `module.exports.describe()` in a module whose whole export IS a
|
||||
// class expression: the receiver is exactly that class (the kept
|
||||
// export assignment pins it) — the direct-name rules apply.
|
||||
@@ -3649,7 +3695,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
`calling the static member '${access.name.text}' through a class value (a subclass of '${info.def.name.replace(/^%|^%m\d+\./, "")}' redeclares it, so the runtime class decides which declaration answers)`,
|
||||
);
|
||||
}
|
||||
const instance = genericCallInstance(lowerer, call, gfound.info);
|
||||
const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, gfound.info, info, throughValue));
|
||||
const args = lowerer.completeArgs(call.arguments, instance.params, loc, call);
|
||||
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
||||
}
|
||||
@@ -3662,7 +3708,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
}
|
||||
const specialized = findGenericStaticOn(lowerer, info, access.name.text);
|
||||
if (specialized?.declarer === found.declarer && specialized.info.implicitParams) {
|
||||
const instance = implicitCallInstance(lowerer, call, specialized.info);
|
||||
const instance = implicitCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, specialized.info, info, throughValue));
|
||||
const args = lowerer.completeArgs(call.arguments, instance.params, loc, call);
|
||||
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
||||
}
|
||||
@@ -3682,12 +3728,44 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
}
|
||||
return { kind: "callValue", callee, args, type: found.field.type.ret, loc };
|
||||
}
|
||||
if (found.method.member.body && hasStaticThis(found.method.member)) {
|
||||
const generic = found.method.receiverInfo ??= {
|
||||
decl: found.method.member, baseName: access.name.text,
|
||||
qualifiedName: `%${found.declarer.def.name}.static:${access.name.text}`,
|
||||
typeParams: [], instances: new Map(), member: { cls: found.declarer, kind: "static" },
|
||||
};
|
||||
const instance = genericCallInstance(lowerer, call, staticMethodForReceiver(lowerer, access, generic, info, throughValue));
|
||||
return { kind: "call", callee: instance.name,
|
||||
args: lowerer.completeArgs(call.arguments, instance.params, loc, call), type: instance.returnType, loc };
|
||||
}
|
||||
const fnName = `%${found.declarer.def.name}.static:${access.name.text}`;
|
||||
lowerer.noteEdge(fnName);
|
||||
const args = lowerer.completeArgs(call.arguments, found.method.params, loc, call);
|
||||
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
|
||||
}
|
||||
|
||||
function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpression, method: GenericFnInfo,
|
||||
receiver: ClassInfo, throughValue: boolean): GenericFnInfo {
|
||||
if (!hasStaticThis(method.decl)) return method;
|
||||
// A private name in the inherited body belongs to the declaring class,
|
||||
// even when the receiver declares a different private with that spelling.
|
||||
// Do not resolve it by receiver name until the static ABI tracks the brand.
|
||||
if (method.member && method.member.cls !== receiver && hasLexicalPrivateReference(method.decl)) {
|
||||
lowerer.unsupported("SC1090", access, "private member references in an inherited receiver-dependent static method");
|
||||
}
|
||||
if (throughValue && !classValueIsExactlyOwn(lowerer, access.expression, receiver)) {
|
||||
lowerer.unsupported("SC1090", access, "receiver-dependent static methods through a polymorphic class value");
|
||||
}
|
||||
const instances = method.receiverSpecializations ??= new Map();
|
||||
let specialized = instances.get(receiver.def.name);
|
||||
if (!specialized) {
|
||||
specialized = { ...method, qualifiedName: `${method.qualifiedName}%on.${receiver.def.name}`,
|
||||
instances: new Map(), staticReceiver: receiver, receiverSpecializations: new Map() };
|
||||
instances.set(receiver.def.name, specialized);
|
||||
}
|
||||
return specialized;
|
||||
}
|
||||
|
||||
/** Static member access through a class VALUE (`X.m` where X is
|
||||
* classval-typed): devirtualized — the member resolves against the
|
||||
* static class's chain, exact when no strict descendant redeclares it
|
||||
@@ -3711,6 +3789,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
}
|
||||
if (
|
||||
!ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.kind !== ts.SyntaxKind.ThisKeyword &&
|
||||
// `module.exports.label` in a class-replaced CJS module: the
|
||||
// receiver is the exact exported class, and the read is
|
||||
// side-effect-free — as bindable as an identifier.
|
||||
@@ -5172,6 +5251,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
||||
if (objectFactory) return objectFactory;
|
||||
let selected: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(selected)) selected = selected.expression;
|
||||
if (ts.isConditionalExpression(selected)) {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.
|
||||
import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget, storedClassValueType } from "./lower-classes.js";
|
||||
import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
|
||||
import { mixinFnOfCallee } from "./lower-mixins.js";
|
||||
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion } from "../type-mapper.js";
|
||||
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion, withUnitArm } from "../type-mapper.js";
|
||||
import { lowerYield } from "./lower-generators.js";
|
||||
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
|
||||
import { countedFor, numLit, varRef } from "../../ir/build.js";
|
||||
@@ -5168,6 +5168,10 @@ export function lowerOptionalNumber(
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
const target = lowerer.fieldTarget(expr);
|
||||
if (target?.container === "class" && target.fieldType.kind === "union" &&
|
||||
lowerer.unions.get(target.fieldType.unionId)?.arms.some(isUnitType)) {
|
||||
return lowerer.fieldGetExpr(target, locOf(expr), expr);
|
||||
}
|
||||
if (target?.container !== "recordOvf") return null;
|
||||
// A JS file-scope object-literal global is record-shaped to the
|
||||
// checker but stored in the checked-dynamic tree to preserve object
|
||||
@@ -6227,7 +6231,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
if (raw.type.kind === "bigint") {
|
||||
return { kind: "libCall", fn: "bigint.neg", args: [raw], type: BIGINT_T, loc };
|
||||
}
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
// Match the checked numeric boundary used by JS binary arithmetic.
|
||||
const operand = raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
|
||||
? { kind: "dynCheck" as const, value: raw, type: F64, loc }
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") lowerer.unsupported("SC1043", expr);
|
||||
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
|
||||
return { kind: "unary", op: "-", operand, type: F64, loc };
|
||||
@@ -7405,15 +7412,17 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// Test the deciding operand in its own representation. The result
|
||||
// contains only the values that can survive short-circuit evaluation.
|
||||
let target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// Unchecked array reads carry undefined even when the checker sees
|
||||
// a required value. The RHS can return it for either operator; the
|
||||
// LHS can return it only for &&. Preserve it through later chain pairs.
|
||||
if (target !== null && !isUnitType(target) && (
|
||||
(right.type.kind === "union" && lowerer.armTag(right.type.unionId, UNDEFINED_T) >= 0) ||
|
||||
(op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" &&
|
||||
lowerer.armTag(left.type.unionId, UNDEFINED_T) >= 0)
|
||||
)) {
|
||||
target = lowerer.runtimeOptionalType(target);
|
||||
// Index reads can be absent without noUncheckedIndexedAccess. Their
|
||||
// explicit nullish fallback still runs, even if the checker discarded it.
|
||||
if (target !== null) {
|
||||
const returned = [right.type, ...(op === ts.SyntaxKind.AmpersandAmpersandToken ? [left.type] : [])];
|
||||
for (const type of returned) {
|
||||
const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms ?? [] : [type];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind === "undefinedT" && !isUnitType(target)) target = lowerer.runtimeOptionalType(target);
|
||||
if (arm.kind === "nullT") target = withUnitArm(target, arm.kind, lowerer.unions) ?? target;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A broad-JSDoc npm-static body can leave the CHECKER result `any`
|
||||
// even after declaration-backed specialization has recovered both
|
||||
@@ -9918,7 +9927,9 @@ function representedClassFieldTarget(
|
||||
* fieldSet/recordSet/accessor-call (minus value/kind) or null. */
|
||||
export function fieldTarget(lowerer: Lowerer, access: ts.PropertyAccessExpression): FieldTarget | null {
|
||||
if (lowerer.chainBlocked(access)) return null;
|
||||
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type : undefined;
|
||||
const receiverIr = stored?.kind === "record" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverIr?.kind === "object") {
|
||||
return classFieldTarget(lowerer, access.expression, receiverIr, access.name.text);
|
||||
}
|
||||
|
||||
@@ -3395,7 +3395,15 @@ export function lowerStaticReadableStreamReaderCall(
|
||||
ts.isSpreadElement(call.arguments[0]!)
|
||||
) {
|
||||
const spread = call.arguments[0]! as ts.SpreadElement;
|
||||
const src = lowerer.lowerExpr(spread.expression);
|
||||
let src = lowerer.lowerExpr(spread.expression);
|
||||
// JavaScript array call results retain checked storage so aliases stay
|
||||
// shared. This read-only fold can extract the inferred numeric array.
|
||||
if (src.type.kind === "dyn") {
|
||||
const inferred = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
|
||||
if (inferred?.kind === "array" && inferred.elem.kind === "f64") {
|
||||
src = lowerer.coerceInto(spread.expression, src, inferred);
|
||||
}
|
||||
}
|
||||
if (src.type.kind !== "array" || src.type.elem.kind !== "f64") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ENTRY_NAME, PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefi
|
||||
import { isNativeFfiRequire, builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireSpecOf, isPromisifyCall, registerBuiltinCallableAlias, stripTypeCasts } from "./lower-builtins.js";
|
||||
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, funcTypeFromParamShapes, implicitLocalFnInfoOf, implicitLocalFnNodeOf, nullishGenericBindingUnitOf, registerOverloadedCallableAlias } from "./lower-calls.js";
|
||||
import { hasJsTypeAnnotation, isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl } from "./lower-stmts.js";
|
||||
import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js";
|
||||
@@ -1609,7 +1610,10 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
// and closures created in the init body capture it normally.
|
||||
// References from separately-declared functions cascade to
|
||||
// their own per-site runtime fences.
|
||||
if (isJsSourceFile(sf) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode))) continue;
|
||||
const factoryType = !isLet && isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) &&
|
||||
ts.isIdentifier(decl.name) && nameNode === decl.name && decl.initializer && ts.isNewExpression(decl.initializer)
|
||||
? objectFactorySignature(lowerer, decl.initializer)?.returnType : undefined;
|
||||
if (!factoryType && isJsSourceFile(sf) && !lowerer.mapTypeOf(lowerer.typeOf(nameNode))) continue;
|
||||
// `var p1 = import("./m")` at file scope: the global holds the
|
||||
// island promise/handle — the import expression's only
|
||||
// production — whatever the checker's namespace type mapped to
|
||||
@@ -1624,7 +1628,11 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
// exactly the local rule (uncheckedOverloadHandleCall).
|
||||
(uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null)
|
||||
: null;
|
||||
let type = handleT ?? lowerer.irTypeOf(nameNode);
|
||||
let type = handleT ?? factoryType ?? lowerer.irTypeOf(nameNode);
|
||||
// JavaScript call results may retain a checked array supplied by
|
||||
// the caller. A typed global here would copy it on extraction.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && type.kind === "array" &&
|
||||
decl.initializer && ts.isCallExpression(decl.initializer)) type = DYN;
|
||||
// Unannotated JavaScript aliases retain an existing native
|
||||
// checked object instead of copying it into an inferred record.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) &&
|
||||
|
||||
@@ -24,6 +24,7 @@ import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
|
||||
import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js";
|
||||
import { isCompiledPrototypeMember } from "./class-prototypes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
@@ -3829,7 +3830,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
type = DYN;
|
||||
}
|
||||
const preservesObjectIdentity = init.type.kind === "dyn" &&
|
||||
(inferredObjectType?.kind === "record" ||
|
||||
(inferredObjectType?.kind === "record" || inferredObjectType?.kind === "array" ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
(inferredObjectType.kind === "func" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) ||
|
||||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
|
||||
@@ -3957,6 +3958,9 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
}
|
||||
if (!type) lowerer.badType(decl.name, lowerer.typeOf(decl.name));
|
||||
let settledType: IrType = type;
|
||||
if (!isLet && isJsSourceFile(decl.getSourceFile()) && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isNewExpression(decl.initializer)) {
|
||||
settledType = objectFactorySignature(lowerer, decl.initializer)?.returnType ?? settledType;
|
||||
}
|
||||
const arithmeticType = decl.initializer ? lowerer.runtimeOptionalArithmeticTypes.get(decl.initializer) : undefined;
|
||||
const isStringArithmeticUnion = (t: IrType): boolean => {
|
||||
if (t.kind !== "union") return false;
|
||||
@@ -5167,7 +5171,7 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
// SUBCLASS name (`D.x = v` creates an OWN property on D in JS —
|
||||
// different storage) and through class VALUES (the same dynamic
|
||||
// story) are named fences, never a silently-wrong global write.
|
||||
if (!expr.left.questionDotToken && ts.isIdentifier(expr.left.expression)) {
|
||||
if (!expr.left.questionDotToken && (ts.isIdentifier(expr.left.expression) || expr.left.expression.kind === ts.SyntaxKind.ThisKeyword)) {
|
||||
// The receiver must BE the class exactly (its name, or a
|
||||
// const binding holding a class expression) — a general class
|
||||
// VALUE could hold a subclass, where JS creates an own
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { bindingNeverReassigned, type FnSig } from "./lower-calls.js";
|
||||
|
||||
/** An ordinary constructor's explicit object return replaces its receiver.
|
||||
* Without observing this or new.target, it can use the function's call ABI. */
|
||||
export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null {
|
||||
if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null;
|
||||
// This probe also sees ordinary classes during global collection. Leave
|
||||
// their deferred diagnostics for the class declaration's lowering.
|
||||
const wasCollecting = lowerer.collecting;
|
||||
let symbol: ts.Symbol | null = null;
|
||||
lowerer.collecting = true;
|
||||
try {
|
||||
if (!lowerer.isTopLevelFnSymbol(expr.expression)) return null;
|
||||
symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
} finally {
|
||||
lowerer.collecting = wasCollecting;
|
||||
}
|
||||
if (!symbol) return null;
|
||||
const declaration = lowerer.checker.declarationsOf(symbol).find((node): node is ts.FunctionDeclaration =>
|
||||
ts.isFunctionDeclaration(node) && node.body !== undefined,
|
||||
);
|
||||
if (!declaration?.body || !bindingNeverReassigned(lowerer, symbol, declaration) || !isJsSourceFile(declaration.getSourceFile()) || declaration.asteriskToken ||
|
||||
declaration.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
|
||||
const last = declaration.body.statements.at(-1);
|
||||
if (!last || !ts.isReturnStatement(last) || !last.expression || !ts.isObjectLiteralExpression(last.expression)) return null;
|
||||
let safe = true;
|
||||
const visit = (node: ts.Node, nestedArrow = false): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { safe = false; return; }
|
||||
if (ts.isFunctionLike(node)) {
|
||||
if (!ts.isArrowFunction(node)) return;
|
||||
nestedArrow = true;
|
||||
}
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword || ts.isMetaProperty(node)) safe = false;
|
||||
if (!nestedArrow && ts.isReturnStatement(node) && (!node.expression || !ts.isObjectLiteralExpression(node.expression))) safe = false;
|
||||
ts.forEachChild(node, (child) => visit(child, nestedArrow));
|
||||
};
|
||||
for (const parameter of declaration.parameters) visit(parameter);
|
||||
visit(declaration.body);
|
||||
if (!safe) return null;
|
||||
const signature = lowerer.fnSigOf(expr.expression);
|
||||
return signature && signature.returnType.kind !== "void" ? signature : null;
|
||||
}
|
||||
|
||||
export function lowerObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
const signature = objectFactorySignature(lowerer, expr);
|
||||
if (!signature) return null;
|
||||
const loc = locOf(expr);
|
||||
lowerer.noteEdge(signature.name);
|
||||
return {
|
||||
kind: "call", callee: signature.name,
|
||||
args: lowerer.completeArgs(expr.arguments ?? [], signature.params, loc, expr),
|
||||
type: signature.returnType, loc,
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export function rejectStaticThis(
|
||||
root: ts.Node,
|
||||
message: (keyword: "this" | "super") => string,
|
||||
includeRoot = false,
|
||||
allowThis = false,
|
||||
): void {
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (
|
||||
@@ -19,7 +20,7 @@ export function rejectStaticThis(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
if ((node.kind === ts.SyntaxKind.ThisKeyword && !allowThis) || node.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
const keyword = node.kind === ts.SyntaxKind.ThisKeyword ? "this" : "super";
|
||||
lowerer.unsupported("SC1090", node, message(keyword));
|
||||
}
|
||||
@@ -28,3 +29,27 @@ export function rejectStaticThis(
|
||||
if (includeRoot) visit(root);
|
||||
else root.forEachChild(visit);
|
||||
}
|
||||
|
||||
export function hasStaticThis(root: ts.Node): boolean {
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) found = true;
|
||||
if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
root.forEachChild(visit);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Private names belong to the lexical class, independently of any
|
||||
* same-spelled member in a receiver's subclass. */
|
||||
export function hasLexicalPrivateReference(root: ts.Node): boolean {
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
|
||||
if (ts.isPrivateIdentifier(node)) found = true;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
root.forEachChild(visit);
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { checkPreflight, loadProgram } from "./program-node.js";
|
||||
|
||||
const source = `
|
||||
export class Base { run() {} }
|
||||
export class Derived extends Base {
|
||||
/** @param {number} value */
|
||||
run(value) { console.log(value); }
|
||||
}
|
||||
export class Other {
|
||||
/** @param {number} value */
|
||||
run(value) { console.log(value); }
|
||||
}
|
||||
/** @template T */
|
||||
export class Box {
|
||||
/** @param {T} value */
|
||||
constructor(value) { this.value = value; }
|
||||
run() {}
|
||||
}
|
||||
/** @extends {Box<number>} */
|
||||
export class NumberBox extends Box { run(value) { console.log(value); } }
|
||||
`;
|
||||
|
||||
test.each([
|
||||
["accepts an inherited JavaScript method with extra parameters", "function take(value: Base) {} take(new Derived());", false],
|
||||
["accepts a base in a union", "function take(value: Base | null) {} take(new Derived());", false],
|
||||
["accepts a constructor parameter", "class Use { constructor(value: Base) {} } new Use(new Derived());", false],
|
||||
["retains unrelated structural errors", "function take(value: Base) {} take(new Other());", true],
|
||||
["retains extra intersection requirements", "function take(value: Base & { required: string }) {} take(new Derived());", true],
|
||||
["retains incompatible generic arguments", "function take(value: Box<string>) {} take(new NumberBox(1));", true],
|
||||
["accepts matching generic arguments", "function take(value: Box<number>) {} take(new NumberBox(1));", false],
|
||||
["retains TypeScript subclass errors", "class Typed extends Base { run(value: number) {} } function take(value: Base) {} take(new Typed());", true],
|
||||
] as const)("%s", (_name, consumer, error) => {
|
||||
const dir = mkdtempSync("/tmp/scriptc-subtyping-");
|
||||
try {
|
||||
const pkg = join(dir, "node_modules", "runtime-classes");
|
||||
mkdirSync(pkg, { recursive: true });
|
||||
writeFileSync(join(pkg, "package.json"), '{"name":"runtime-classes","type":"module","main":"index.js"}');
|
||||
writeFileSync(join(pkg, "index.js"), source);
|
||||
writeFileSync(join(dir, "package.json"), '{"type":"module"}');
|
||||
writeFileSync(join(dir, "main.ts"), 'import { Base, Derived, Other, Box, NumberBox } from "runtime-classes";\n' + consumer);
|
||||
const load = loadProgram(join(dir, "main.ts"), { npmStatic: ["runtime-classes"] });
|
||||
try {
|
||||
const diagnostics = checkPreflight(load);
|
||||
if (error) expect(diagnostics.some((d) => d.code === "SC0001" && d.message.includes("not assignable")), JSON.stringify(diagnostics)).toBe(true);
|
||||
else expect(diagnostics).toEqual([]);
|
||||
} finally { load.dispose(); }
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { npmStaticPackageOfPath } from "./npm-static.js";
|
||||
|
||||
/** JS subclasses can extend inferred fields and method arities. The native
|
||||
* class collector validates their shared layout and dispatch ABI; a consumer
|
||||
* upcast should not fail solely on the checker's structural override model. */
|
||||
export function isNpmStaticSubclassArgument(program: ts.Program, diagnostic: ts.Diagnostic): boolean {
|
||||
if (diagnostic.code !== 2345 || diagnostic.fileName === undefined) return false;
|
||||
const source = program.getSourceFile(diagnostic.fileName);
|
||||
if (!source) return false;
|
||||
const checker = program.getTypeChecker();
|
||||
const runtimeClass = (type: ts.Type): boolean => {
|
||||
const symbol = type.getSymbol();
|
||||
return symbol !== undefined && checker.declarationsOf(symbol).some((declaration) =>
|
||||
(ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) &&
|
||||
/\.[cm]?js$/.test(declaration.getSourceFile().fileName) &&
|
||||
npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null,
|
||||
);
|
||||
};
|
||||
const subtype = (actual: ts.Type, expected: ts.Type): boolean => {
|
||||
if (!runtimeClass(actual)) return false;
|
||||
const targets = expected.isUnionType() ? ts.constituentTypes(expected) : [expected];
|
||||
const seen = new Set<ts.Type>();
|
||||
const visit = (type: ts.Type): boolean => {
|
||||
const target = type.isTypeReference() ? type.getTarget() : type;
|
||||
if (!target?.isClassOrInterface() || seen.has(target)) return false;
|
||||
seen.add(target);
|
||||
for (const base of checker.getBaseTypes(type.isClassOrInterface() ? type : target)) {
|
||||
if (targets.some((expected) => runtimeClass(expected) && base.getSymbol() === expected.getSymbol() && checker.isTypeAssignableTo(base, expected))) return true;
|
||||
if (visit(base)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return visit(actual);
|
||||
};
|
||||
let accepted = false;
|
||||
ts.walkPreorder(source, (node) => {
|
||||
if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip";
|
||||
if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined;
|
||||
const index = node.arguments?.findIndex((argument) => argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end) ?? -1;
|
||||
if (index < 0) return undefined;
|
||||
const parameter = checker.getResolvedSignature(node)?.getParameters()[index];
|
||||
if (parameter && node.arguments) accepted = subtype(checker.getTypeAtLocation(node.arguments[index]!), checker.getTypeOfSymbol(parameter));
|
||||
return undefined;
|
||||
});
|
||||
return accepted;
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import { isNodeModulesPath, nearestInvalidPackageJsonPath, nearestPackageType, n
|
||||
import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js";
|
||||
import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticDeclarationOverloads, setNpmStaticPackages } from "./npm-static.js";
|
||||
import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.js";
|
||||
import { isNpmStaticSubclassArgument } from "./npm-static-subtyping.js";
|
||||
import { npmStaticDeclarationReexports, npmStaticRuntimeClassTargets, parseNpmStaticDeclarationOverloads, parseNpmStaticDeclarationProperties } from "./npm-static-declaration-syntax.js";
|
||||
import type { FrontendServices } from "./services.js";
|
||||
import type { NpmStaticDeclarationOverloads, NpmStaticDeclarationProperties, NpmStaticOverloadSignature } from "./npm-static-declaration-syntax.js";
|
||||
@@ -2382,6 +2383,7 @@ function preflight7(load: LoadResult): {
|
||||
!npmStaticFileSuppressed(d) &&
|
||||
!nodeModulesJsSuppressed(d) &&
|
||||
!namespaceCalleeSuppressed(p, d) &&
|
||||
!isNpmStaticSubclassArgument(p, d) &&
|
||||
!workspaceImplicitAnySuppressed(p, d) &&
|
||||
!jsdocTypeSuppressed(p, d, commentDup),
|
||||
);
|
||||
|
||||
@@ -87,6 +87,9 @@ export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
|
||||
2308, 2614,
|
||||
// 2740's sibling elaboration ("Property X is missing in type Y")
|
||||
2741,
|
||||
// JavaScript subclasses may widen fields or add method parameters.
|
||||
// Class lowering checks native storage and override ABIs at each use.
|
||||
2415,
|
||||
// unresolvable modules: Node throws WHEN the require executes — the
|
||||
// binding types as any and reached uses fence (never a silent pass)
|
||||
2307, 2792,
|
||||
|
||||
@@ -3551,12 +3551,18 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve
|
||||
* admits, `(() => void) | undefined`) so the interned union is IDENTICAL
|
||||
* to what mapping the checker's own `T | undefined` produces. */
|
||||
export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | null {
|
||||
return withUnitArm(t, "undefinedT", unions);
|
||||
}
|
||||
|
||||
/** Preserve a nullish runtime value that JavaScript inference omitted. */
|
||||
export function withUnitArm(t: IrType, kind: "nullT" | "undefinedT", unions: UnionRegistry): IrType | null {
|
||||
const unit: IrType = { kind };
|
||||
if (t.kind === "union") {
|
||||
const def = unions.get(t.unionId);
|
||||
if (!def) return null;
|
||||
if (def.arms.some((a) => a.kind === "date")) return null;
|
||||
if (def.arms.some((a) => a.kind === "undefinedT")) return t;
|
||||
const arms = [...def.arms, UNDEFINED_T];
|
||||
if (def.arms.some((a) => a.kind === kind)) return t;
|
||||
const arms = [...def.arms, unit];
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: unions.transform(def, arms) };
|
||||
}
|
||||
@@ -3568,7 +3574,7 @@ export function withUndefinedArm(t: IrType, unions: UnionRegistry): IrType | nul
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const arms = [t, UNDEFINED_T];
|
||||
const arms = [t, unit];
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: unions.intern(arms) };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze } from "../src/index.js";
|
||||
|
||||
test.each([
|
||||
["constructor reads this", "function Make() { console.log(this); return { value: 1 }; } new Make();"],
|
||||
["constructor arrow captures this", "function Make() { return { read: () => this }; } new Make();"],
|
||||
["constructor default reads this", "function Make({value = this} = {}) { return { value }; } new Make();"],
|
||||
["constructor reads new.target", "function Make() { console.log(new.target); return { value: 1 }; } new Make();"],
|
||||
["constructor returns a primitive on one branch", "function Make(flag) { if (flag) return 1; return { value: 1 }; } new Make(true);"],
|
||||
["constructor binding reassigned", "function Make() { return { value: 1 }; } Make = function () { return { value: 2 }; }; console.log(new Make().value);"],
|
||||
] as const)("object-returning constructor retains the fence when %s", (_name, source) => {
|
||||
const dir = mkdtempSync("/tmp/scriptc-constructor-boundary-");
|
||||
try {
|
||||
const entry = join(dir, "main.mjs");
|
||||
writeFileSync(entry, source);
|
||||
const { coverage } = analyze(entry);
|
||||
expect([...coverage.diagnostics, ...(coverage.runtimeFences ?? [])].length).toBeGreaterThan(0);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test.each([
|
||||
["detached method", "class C { static value = 1; static read(): number { return this.value; } } const read = C.read; console.log(read());"],
|
||||
["polymorphic receiver", "class C { static read(): string { return this.name; } } class D extends C {} function run(value: typeof C): string { return value.read(); } console.log(run(D));"],
|
||||
["inherited private field", "class C { static #value = 1; static read(): number { return this.#value; } } class D extends C {} console.log(D.read());"],
|
||||
["shadowed private field", "class C { static #value = 1; static read(): number { return this.#value; } } class D extends C { static #value = 2; } console.log(D.read());"],
|
||||
["inherited private method", "class C { static #value(): number { return 1; } static read(): number { return this.#value(); } } class D extends C {} console.log(D.read());"],
|
||||
["shadowed private method", "class C { static #value(): number { return 1; } static read(): number { return this.#value(); } } class D extends C { static #value(): number { return 2; } } console.log(D.read());"],
|
||||
["inherited static write", "class C { static value = 1; static write(): void { this.value = 2; } } class D extends C {} D.write();"],
|
||||
] as const)("receiver specialization preserves the %s boundary", (_name, source) => {
|
||||
const dir = mkdtempSync("/tmp/scriptc-static-boundary-");
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
writeFileSync(entry, source);
|
||||
const { coverage } = analyze(entry);
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.diagnostics.some(d => d.code === "SC1090"), JSON.stringify(coverage.diagnostics)).toBe(true);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -9122,12 +9122,24 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-chained-native-results.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-chained-native-results.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-checked-array-reference.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-checked-array-reference.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-checked-number-negation.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-checked-number-negation.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-class-added-parameters.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-class-added-parameters.js"
|
||||
@@ -9202,6 +9214,18 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-nullish-inherited-fields.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-nullish-inherited-fields.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-object-returning-constructors.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-object-returning-constructors.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-object-shared-arrays.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-object-shared-arrays.js"
|
||||
@@ -9768,6 +9792,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/static-method-receivers.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/static-method-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/stdio-values.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/stdio-values.mjs"
|
||||
|
||||
Generated
+3
@@ -23,6 +23,9 @@ importers:
|
||||
js-yaml:
|
||||
specifier: 4.3.0
|
||||
version: 4.3.0
|
||||
rollup:
|
||||
specifier: 4.62.2
|
||||
version: 4.62.2
|
||||
three:
|
||||
specifier: 0.186.1
|
||||
version: 0.186.1
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const Vector = class Vector {
|
||||
constructor(x = 0) { this.x = x; }
|
||||
copy(value) { this.x = value.x; return this; }
|
||||
add(value) { this.x += value.x; return this; }
|
||||
};
|
||||
let calls = 0;
|
||||
function make() { calls++; return new Vector(); }
|
||||
const result = make().copy(new Vector(2)).add(new Vector(3));
|
||||
console.log(result.x, calls);
|
||||
console.log(make().copy(result).add(result).x, calls);
|
||||
@@ -0,0 +1,4 @@
|
||||
const input = JSON.parse('[3, -4, 0, -0]');
|
||||
for (const value of input) console.log(-value, Object.is(-value, -0));
|
||||
const object = { get() { return JSON.parse('5'); } };
|
||||
console.log(-object.get());
|
||||
@@ -0,0 +1,40 @@
|
||||
class Value {
|
||||
constructor() { this.id = 7; }
|
||||
}
|
||||
|
||||
class Base {
|
||||
/** @param {Record<string, Value>} values */
|
||||
load(values) { this.value = values.missing || null; }
|
||||
}
|
||||
|
||||
class Derived extends Base {
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {?Value} */
|
||||
this.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
const base = new Base();
|
||||
console.log('initial', base.value === undefined);
|
||||
base.load({});
|
||||
console.log('missing', base.value === null);
|
||||
const value = new Value();
|
||||
base.load({ missing: value });
|
||||
console.log('present', base.value === value);
|
||||
const derived = new Derived();
|
||||
console.log('derived', derived.value === null);
|
||||
derived.load({ missing: value });
|
||||
console.log('identity', derived.value === value);
|
||||
derived.load({});
|
||||
console.log('reset', derived.value === null);
|
||||
|
||||
class Optional {
|
||||
/** @param {Value} [value] */
|
||||
constructor(value) {
|
||||
/** @type {Value} */
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
console.log('omitted', new Optional().value === undefined);
|
||||
console.log('supplied', new Optional(value).value === value);
|
||||
@@ -0,0 +1,23 @@
|
||||
function Counter(start = 2) {
|
||||
let value = start;
|
||||
return {
|
||||
next() { return ++value; },
|
||||
current() { return value; },
|
||||
};
|
||||
}
|
||||
|
||||
let calls = 0;
|
||||
function initial() { calls++; return 10; }
|
||||
const first = new Counter(initial());
|
||||
const second = new Counter();
|
||||
console.log(calls, first.current(), second.current());
|
||||
console.log(first.next(), first.next(), second.next());
|
||||
console.log(first === second);
|
||||
function readFirst() { return first.current(); }
|
||||
console.log('global', readFirst());
|
||||
|
||||
function Choice(flag = false) {
|
||||
if (flag) return { value: 3 };
|
||||
return { value: 9 };
|
||||
}
|
||||
console.log(new Choice(true).value, new Choice().value);
|
||||
@@ -0,0 +1,24 @@
|
||||
class Base {
|
||||
static label = "base";
|
||||
static scale(value: number): number { return value * 2; }
|
||||
static run(value: number): number { return this.scale(value) + 1; }
|
||||
static describe(label: string = this.label): string { return this.name + ":" + label; }
|
||||
static rename(label: string): void { this.label = label; }
|
||||
static delayed(value: number): () => number { return () => this.run(value); }
|
||||
static generic<T>(value: T): T { console.log(this.run(2)); return value; }
|
||||
}
|
||||
class Derived extends Base {
|
||||
static label = "derived";
|
||||
static scale(value: number): number { return value * 3; }
|
||||
}
|
||||
console.log(Base.run(4), Derived.run(4));
|
||||
console.log(Base.delayed(5)(), Derived.delayed(5)());
|
||||
console.log(Base.generic('base'), Derived.generic('derived'));
|
||||
console.log(Base.describe(), Derived.describe(), Derived.describe('custom'));
|
||||
Base.rename('first'); Derived.rename('second');
|
||||
console.log(Base.describe(), Derived.describe());
|
||||
class Private {
|
||||
static #value = 7;
|
||||
static read(): number { return this.#value; }
|
||||
}
|
||||
console.log(Private.read());
|
||||
@@ -69,10 +69,12 @@ let pick: typeof Left | typeof Right = Left;
|
||||
const picked = new pick();
|
||||
void picked;
|
||||
|
||||
// `this` in a static method names the RECEIVER class — a dynamic value.
|
||||
// A polymorphic class value cannot specialize a receiver-dependent method.
|
||||
class UsesThis {
|
||||
static self(): string {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
console.log(UsesThis.self());
|
||||
class OtherThis extends UsesThis {}
|
||||
function self(value: typeof UsesThis): string { return value.self(); }
|
||||
console.log(self(OtherThis));
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class Item {
|
||||
constructor(value = 1) { this.value = value; }
|
||||
}
|
||||
class Collector {
|
||||
/** @param {Array<Item>} output */
|
||||
collect(output) { output.push(new Item(3)); }
|
||||
/** @param {Array<Item>} output
|
||||
* @return {Array<Item>} */
|
||||
append(output) { this.collect(output); return output; }
|
||||
run() {
|
||||
const output = [];
|
||||
this.collect(output);
|
||||
console.log('local', output.length, output[0].value);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
const collector = new Collector();
|
||||
const values = [];
|
||||
collector.collect(values);
|
||||
const alias = collector.append(values);
|
||||
console.log('shared', values.length, alias === values, values[0].value);
|
||||
console.log('nested', collector.run().length);
|
||||
alias.push(new Item(7));
|
||||
function readShared() { return alias.length + values[2].value; }
|
||||
console.log('write-back', readShared());
|
||||
/** @returns {Array<Item>} */
|
||||
function seeded() { return [new Item(4)]; }
|
||||
const typed = seeded();
|
||||
collector.collect(typed);
|
||||
console.log('typed-result', typed.length, typed[0].value, typed[1].value);
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { Vector3, Triangle, Sphere, Ray } from 'three';
|
||||
import { Capsule } from 'three/addons/math/Capsule.js';
|
||||
import { Octree } from 'three/addons/math/Octree.js';
|
||||
|
||||
const round = n => Math.round(n * 1000000);
|
||||
const tree = new Octree();
|
||||
tree.trianglesPerLeaf = 2;
|
||||
for (let x = -4; x < 4; x++) {
|
||||
for (let z = -4; z < 4; z++) {
|
||||
tree.addTriangle(new Triangle(new Vector3(x,0,z),new Vector3(x,0,z+1),new Vector3(x+1,0,z)));
|
||||
tree.addTriangle(new Triangle(new Vector3(x+1,0,z),new Vector3(x,0,z+1),new Vector3(x+1,0,z+1)));
|
||||
}
|
||||
}
|
||||
tree.build();
|
||||
const body = new Capsule(new Vector3(0.1,0.2,0.1),new Vector3(0.1,1.3,0.1),0.35);
|
||||
const candidates = [];
|
||||
tree.getCapsuleTriangles(body,candidates);
|
||||
console.log('candidates', candidates.length, new Set(candidates).size);
|
||||
const hit = tree.capsuleIntersect(body);
|
||||
if (hit) console.log('capsule',round(hit.depth),round(hit.normal.y),round(body.start.y));
|
||||
else console.log('capsule miss');
|
||||
body.translate(new Vector3(0,3,0));
|
||||
console.log('capsule miss',tree.capsuleIntersect(body) === false);
|
||||
const sphere = tree.sphereIntersect(new Sphere(new Vector3(0.1,0.2,0.1),0.4));
|
||||
if(sphere) console.log('sphere',round(sphere.depth),round(sphere.normal.y));
|
||||
else console.log('sphere miss');
|
||||
const ray = tree.rayIntersect(new Ray(new Vector3(0.2,4,0.3),new Vector3(0,-1,0)));
|
||||
if(ray) console.log('ray',round(ray.distance),round(ray.position.x),round(ray.position.y));
|
||||
else console.log('ray miss');
|
||||
console.log('ray miss',tree.rayIntersect(new Ray(new Vector3(8,4,8),new Vector3(0,-1,0))) === false);
|
||||
const a = new Vector3(0,0,0), b = new Vector3(0,0,1), c = new Vector3(1,0,0);
|
||||
console.log('inside', Triangle.containsPoint(new Vector3(0.2,0.1,0.2),a,b,c));
|
||||
console.log('outside', Triangle.containsPoint(new Vector3(2,0,2),a,b,c));
|
||||
console.log('degenerate', Triangle.containsPoint(new Vector3(),a,a,a));
|
||||
@@ -79,9 +79,9 @@ class-values.ts:69:16 - error SC1090: constructing through a union of class valu
|
||||
| ^~~~~~~~~~
|
||||
70 | void picked;
|
||||
|
||||
class-values.ts:75:12 - error SC1090: 'this' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead) are not supported yet
|
||||
class-values.ts:79:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet
|
||||
|
||||
74 | static self(): string {
|
||||
75 | return this.name;
|
||||
| ^~~~
|
||||
76 | }
|
||||
78 | class OtherThis extends UsesThis {}
|
||||
79 | function self(value: typeof UsesThis): string { return value.self(); }
|
||||
| ^~~~~~~~~~
|
||||
80 | console.log(self(OtherThis));
|
||||
@@ -0,0 +1,58 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { rollup } from "rollup";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const hasZig = spawnSync("zig", ["version"], { stdio: "ignore" }).status === 0;
|
||||
const require = createRequire(import.meta.url);
|
||||
const wasiRunner = `const {WASI}=require('node:wasi'); const {readFileSync}=require('node:fs');
|
||||
const wasi=new WASI({version:'preview1',args:[process.argv[1]],returnOnExit:true});
|
||||
const instance=new WebAssembly.Instance(new WebAssembly.Module(readFileSync(process.argv[1])),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;
|
||||
const stderr = result.stderr.split("\n").filter(line => !line.startsWith("scriptc RC audit skipped:")).join("\n");
|
||||
return { stdout: result.stdout, stderr, status: result.status, signal: result.signal };
|
||||
}
|
||||
|
||||
test.for(["native", "wasm32-wasi"])("published spatial queries and output arrays run statically on %s", async (target, context) => {
|
||||
if (target === "wasm32-wasi" && !hasZig) context.skip();
|
||||
const dir = await mkdtemp("/tmp/scriptc-spatial-");
|
||||
const previousTarget = process.env["SCRIPTC_TARGET"], previousCc = process.env["SCRIPTC_CC"];
|
||||
try {
|
||||
if(target === "wasm32-wasi") { process.env["SCRIPTC_TARGET"] = target; process.env["SCRIPTC_CC"] = "zigcc"; }
|
||||
const pkg = join(dir,"node_modules","spatial-fixture");
|
||||
await mkdir(pkg,{recursive:true});
|
||||
await writeFile(join(pkg,"package.json"),'{"name":"spatial-fixture","type":"module","main":"index.js"}');
|
||||
const entry = join(dir,"main.mjs");
|
||||
await writeFile(entry,'import "spatial-fixture";');
|
||||
for (const fixture of ["three/collision.mjs", "npm-static/array-output-parameters.mjs"]) {
|
||||
const source = join(import.meta.dirname,"../fixtures",fixture);
|
||||
const bundle = await rollup({ input:source, plugins:[{name:"three-cpu-entry",resolveId(id) {
|
||||
if(id === "three") return require.resolve("three/src/Three.Core.js");
|
||||
if(id.startsWith("three/")) return require.resolve(id);
|
||||
return null;
|
||||
}}] });
|
||||
try { await bundle.write({file:join(pkg,"index.js"),format:"es"}); }
|
||||
finally { await bundle.close(); }
|
||||
const wasm = target === "wasm32-wasi";
|
||||
const output = join(dir,wasm ? "program.wasm" : "program");
|
||||
const result = await compile(entry,{outDir:dir,outPath:output,backend:"llvm",dynamic:false,npmStatic:["spatial-fixture"],
|
||||
sanitize:!wasm && process.env["SCRIPTC_SAN"] === "1"});
|
||||
if(!result.ok) throw new Error(result.diagnostics.map(d=>`${d.code}: ${d.message}`).join("\n"));
|
||||
if(wasm) expect([...(await readFile(output)).subarray(0,4)]).toEqual([0,97,115,109]);
|
||||
const reference = run(process.execPath,["--no-warnings",source]);
|
||||
expect(reference.status).toBe(0);
|
||||
expect(run(process.execPath,["--no-warnings",entry])).toEqual(reference);
|
||||
expect(wasm ? run(process.execPath,["--no-warnings","-e",wasiRunner,output]) : run(output,[])).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