mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Compile three.js scenes, cameras, and geometry
- Compile scene hierarchies, synchronized rotations, and perspective camera transforms. - Support box and buffer geometry, bounds, normals, and index selection. - Preserve JavaScript constructor and callback behavior in native LLVM and WASM builds.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, VOID, isClassOwnEnumerableFieldName, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { dynUndefinedExpr, type Lowerer } from "./lowerer.js";
|
||||
import { findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
|
||||
/** Zero-argument notification methods can be replaced by own callbacks.
|
||||
* Other compiled signatures retain their method-replacement fence. */
|
||||
export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
if (!isClassOwnEnumerableFieldName(name) || name.startsWith("get:") || name.startsWith("set:")) return false;
|
||||
if (info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream || info.fields.has(name)) return false;
|
||||
const found = findMethodOn(lowerer, info, name);
|
||||
return !!found && found.sig.params.length === 0 && found.sig.ret.kind === "void" && !found.sig.abstract;
|
||||
}
|
||||
|
||||
function callbackBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
}
|
||||
|
||||
export function lowerClassCallbackAssign(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt | null {
|
||||
const access = expr.left;
|
||||
if (!ts.isPropertyAccessExpression(access) || access.questionDotToken) return null;
|
||||
const receiver = tryLowerExpression(lowerer, access.expression);
|
||||
if (!receiver) return null;
|
||||
const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
|
||||
if (!info || !isClassCallback(lowerer, info, access.name.text)) return null;
|
||||
const loc = locOf(expr);
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [callbackBag(lowerer, receiver, loc),
|
||||
{ kind: "strLit", value: access.name.text, type: STRING, loc }, lowerer.lowerExprExpecting(expr.right, DYN)], type: VOID, loc }, loc };
|
||||
}
|
||||
|
||||
/** Read the own property before arguments run, retaining callback identity
|
||||
* and the original native receiver for JavaScript's `this` binding. */
|
||||
export function classCallbackValue(lowerer: Lowerer, receiver: IrExpr, name: string, fallback: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
|
||||
const value = varRef(bag.id, DYN, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const result: IrExpr = { kind: "ternary", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [value, key], type: BOOL, loc },
|
||||
then: { kind: "dynKeyGet", value, key, type: DYN, loc }, else_: lowerer.coerceToExpected(fallback, DYN), type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: bag.id, init: callbackBag(lowerer, receiver, loc), loc }], result, type: DYN, loc };
|
||||
}
|
||||
|
||||
export function classCallbackCall(lowerer: Lowerer, receiver: IrExpr, name: string, args: IrExpr[], fallback: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const bag = lowerer.declareHiddenLocal("%callbackBag", DYN);
|
||||
const own = lowerer.declareHiddenLocal("%callbackOwn", BOOL);
|
||||
const callback = lowerer.declareHiddenLocal("%callback", DYN);
|
||||
const value = varRef(bag.id, DYN, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const call: IrExpr = { kind: "dynCall", callee: varRef(callback.id, DYN, loc), receiver: lowerer.coerceToExpected(receiver, DYN),
|
||||
calleeName: name, args, type: DYN, loc };
|
||||
const result: IrExpr = { kind: "ternary", cond: varRef(own.id, BOOL, loc), then: call,
|
||||
else_: { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: fallback, loc }], result: dynUndefinedExpr(loc), type: DYN, loc }, type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [
|
||||
{ kind: "varDecl", localId: bag.id, init: callbackBag(lowerer, receiver, loc), loc },
|
||||
{ kind: "varDecl", localId: own.id, init: { kind: "libCall", fn: "dyn.hasOwn", args: [value, key], type: BOOL, loc }, loc },
|
||||
{ kind: "varDecl", localId: callback.id, init: { kind: "dynKeyGet", value, key, type: DYN, loc }, loc },
|
||||
], result, type: DYN, loc };
|
||||
}
|
||||
@@ -11,17 +11,34 @@ function literalName(name: ts.PropertyName): string | null {
|
||||
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
|
||||
}
|
||||
|
||||
function bagOnlyProperty(lowerer: Lowerer, owner: ClassInfo, name: string): boolean {
|
||||
return !owner.fields.has(name) && !isCompiledPrototypeMember(lowerer, owner, name) &&
|
||||
owner.subclasses.every((child) => bagOnlyProperty(lowerer, child, name));
|
||||
}
|
||||
|
||||
/** A named bag property needs no snapshot of unrelated native fields,
|
||||
* which may contain recursive or otherwise opaque values. */
|
||||
export function lowerClassDescriptorRead(lowerer: Lowerer, call: ts.CallExpression, target: IrExpr): IrExpr | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
const key = call.arguments[1]!;
|
||||
if (!info || info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream ||
|
||||
!ts.isStringLiteral(key) || !bagOnlyProperty(lowerer, info, key.text)) return null;
|
||||
const loc = locOf(call);
|
||||
const bag: IrExpr = { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(target, DYN)], type: DYN, loc };
|
||||
return { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args: [bag, lowerer.lowerExprExpecting(key, DYN)], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** Native layout fields cannot change descriptors. New named data properties
|
||||
* live in the instance's shared bag, preserving attributes and identity. */
|
||||
export function lowerClassDataDescriptor(lowerer: Lowerer, call: ts.CallExpression, member: string, target: IrExpr): IrExpr | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
if (!info || info.def.runtime || info.builtinError || info.builtinEmitter || info.builtinStream) return null;
|
||||
const safeName = (owner: ClassInfo, name: string): boolean =>
|
||||
!owner.fields.has(name) && !isCompiledPrototypeMember(lowerer, owner, name) &&
|
||||
owner.subclasses.every((child) => safeName(child, name));
|
||||
const safeName = (owner: ClassInfo, name: string): boolean => bagOnlyProperty(lowerer, owner, name);
|
||||
const descriptor = (node: ts.Expression): boolean => ts.isObjectLiteralExpression(node) &&
|
||||
node.properties.every((p) => ts.isPropertyAssignment(p) &&
|
||||
node.properties.every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
["value", "writable", "enumerable", "configurable"].includes(literalName(p.name) ?? ""));
|
||||
const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!;
|
||||
if (member === "defineProperty") {
|
||||
|
||||
@@ -4,13 +4,15 @@ import { varRef } from "../../ir/build.js";
|
||||
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 { accessorCall, findGenericMethodOn, findMethodOn, genericOverrideBelow, upcastTo, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPrototypeData, hasClassPrototypeData } from "./class-prototypes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
|
||||
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
|
||||
interface Dispatch {
|
||||
source: Invoke;
|
||||
fn: IrFunction;
|
||||
callback: IrFunction;
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
@@ -21,6 +23,16 @@ interface PropertyDispatch {
|
||||
classes: Set<string>;
|
||||
}
|
||||
|
||||
export function classInstanceOf(lowerer: Lowerer, value: IrExpr, info: ClassInfo, loc: SrcLoc): IrExpr {
|
||||
const name = `%dyn.class.instanceof:${info.def.name}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
|
||||
name, params: [{ localId: "value", name: "value", type: DYN }], returnType: BOOL,
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }], loc,
|
||||
});
|
||||
return { kind: "call", callee: name, args: [value], type: BOOL, loc };
|
||||
}
|
||||
|
||||
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc): IrFunction {
|
||||
const name = "%dyn.class.properties";
|
||||
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
|
||||
@@ -47,6 +59,7 @@ export class ClassDynamicDispatch {
|
||||
private readonly bagClasses = new Set<string>();
|
||||
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
private readonly instanceTests = new Map<string, Set<string>>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
const seenTypes = new Set<string>();
|
||||
@@ -85,6 +98,26 @@ export class ClassDynamicDispatch {
|
||||
});
|
||||
if (this.boxed.size === 0) return false;
|
||||
let changed = false;
|
||||
for (const fn of functions) {
|
||||
if (!fn.name.startsWith("%dyn.class.instanceof:")) continue;
|
||||
const target = fn.name.slice("%dyn.class.instanceof:".length);
|
||||
let checked = this.instanceTests.get(fn.name);
|
||||
if (!checked) { checked = new Set(); this.instanceTests.set(fn.name, checked); }
|
||||
for (const className of this.boxed) {
|
||||
if (checked.has(className)) continue;
|
||||
checked.add(className);
|
||||
const subtype = className === target || lowerer.isSubclassOf(className, target);
|
||||
if (!subtype && !lowerer.isSubclassOf(target, className)) continue;
|
||||
const loc = fn.loc;
|
||||
const value = varRef("value", DYN, loc);
|
||||
const type: IrType = { kind: "object", className };
|
||||
const result: IrExpr = subtype ? { kind: "boolLit", value: true, type: BOOL, loc }
|
||||
: { kind: "instanceOf", value: { kind: "dynCheck", value, type, loc }, className: target, type: BOOL, loc };
|
||||
fn.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: "return", value: result, loc }], else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!this.propertyBag) {
|
||||
this.propertyBag = classPropertiesHelper(lowerer, functions[0]!.loc);
|
||||
this.generated.add(this.propertyBag);
|
||||
@@ -220,19 +253,33 @@ export class ClassDynamicDispatch {
|
||||
let dispatch = this.dispatches.get(key);
|
||||
if (!dispatch) {
|
||||
const params = [expr.recv, ...expr.args].map((_, i) => ({ localId: `p.${i}`, name: `p${i}`, type: DYN }));
|
||||
params.splice(1, 0, { localId: "p.callback", name: "callback", type: DYN });
|
||||
const callback: IrFunction = {
|
||||
name: `%dyn.class.callback.${this.dispatches.size}`, params: [params[0]!], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "receiver", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value: dynUndefinedExpr(expr.loc), loc: expr.loc }], loc: expr.loc,
|
||||
};
|
||||
const helper: IrFunction = {
|
||||
name: `%dyn.class.call.${this.dispatches.size}`, params, returnType: DYN,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: DYN, mutable: false })),
|
||||
body: [{ kind: "return", value: { ...expr, recv: varRef("p.0", DYN, expr.loc), args: expr.args.map((_, i) => varRef(`p.${i + 1}`, DYN, expr.loc)) }, loc: expr.loc }],
|
||||
loc: expr.loc,
|
||||
};
|
||||
dispatch = { source: expr, fn: helper, classes: new Set() };
|
||||
dispatch = { source: expr, fn: helper, callback, classes: new Set() };
|
||||
this.dispatches.set(key, dispatch);
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
this.generated.add(callback);
|
||||
lowerer.liftedFns.push(helper, callback);
|
||||
changed = true;
|
||||
}
|
||||
return { kind: "call", callee: dispatch.fn.name, args: [expr.recv, ...expr.args], type: DYN, loc: expr.loc };
|
||||
// Resolve an own callback before argument effects can replace it.
|
||||
const local = { id: `%dispatch.receiver.${fn.locals.length}`, name: "receiver", type: DYN, mutable: false };
|
||||
fn.locals.push(local);
|
||||
const receiver = varRef(local.id, DYN, expr.loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: expr.recv, loc: expr.loc }], result: {
|
||||
kind: "call", callee: dispatch.fn.name, args: [receiver,
|
||||
{ kind: "call", callee: dispatch.callback.name, args: [receiver], type: DYN, loc: expr.loc }, ...expr.args], type: DYN, loc: expr.loc,
|
||||
}, type: DYN, loc: expr.loc };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -243,6 +290,13 @@ export class ClassDynamicDispatch {
|
||||
const loc = dispatch.source.loc;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const receiver = varRef("p.0", DYN, loc);
|
||||
if (isClassCallback(lowerer, info, dispatch.source.method)) {
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [receiver], type: DYN, loc };
|
||||
dispatch.callback.body.unshift({ kind: "if", cond: {
|
||||
kind: "libCall", fn: "dyn.typedRefIs", args: [receiver, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc,
|
||||
}, then: [{ kind: "return", value: { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args: [bag,
|
||||
lowerer.coerceToExpected({ kind: "strLit", value: dispatch.source.method, type: STRING, loc }, DYN)], type: DYN, loc }, loc }], else_: null, loc });
|
||||
}
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
try {
|
||||
@@ -410,7 +464,7 @@ export class ClassDynamicDispatch {
|
||||
result = methodInfo.sig.ret;
|
||||
callee = `%${owner.def.name}.${method}`;
|
||||
virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true;
|
||||
} else if (generic?.info.implicitParams && generic.declarer.decl && !lowerer.inHierarchy(info)) {
|
||||
} else if (generic?.info.implicitParams && generic.declarer.decl && !genericOverrideBelow(lowerer, info, method)) {
|
||||
owner = generic.declarer;
|
||||
// A failed eager specialization leaves a signature in the instance
|
||||
// cache, but no body. Later dispatch arities must retain the fence
|
||||
@@ -461,6 +515,14 @@ export class ClassDynamicDispatch {
|
||||
if (virtual) lowerer.noteVirtualEdge(owner, method);
|
||||
else lowerer.noteEdge(callee);
|
||||
}
|
||||
if (isClassCallback(lowerer, info, method)) {
|
||||
const descriptor = varRef("p.callback", DYN, loc);
|
||||
body.unshift({ kind: "if", cond: { kind: "dynTest", test: "undefined", negated: true, value: descriptor, type: BOOL, loc }, then: [
|
||||
{ kind: "return", value: { kind: "dynCall", callee: { kind: "dynKeyGet", value: descriptor,
|
||||
key: { kind: "strLit", value: "value", type: STRING, loc }, type: DYN, loc }, receiver: varRef("p.0", DYN, loc),
|
||||
args: incoming, calleeName: dispatch.source.calleeName, type: DYN, loc }, loc },
|
||||
], else_: null, loc });
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { BOOL, DYN, STRING, VOID, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { classValueRef, upcastTo, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
|
||||
/** Select the instance's concrete class before evaluating constructor
|
||||
* arguments. Each branch completes arguments against its own constructor,
|
||||
* including subclasses with different defaults or parameter counts. */
|
||||
export function lowerInstanceConstructorNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
|
||||
let access = expr.expression;
|
||||
while (ts.isParenthesizedExpression(access)) access = access.expression;
|
||||
if (!ts.isPropertyAccessExpression(access) || access.name.text !== "constructor" || access.questionDotToken) return null;
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
if (receiver.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(receiver.type.className);
|
||||
if (!info || info.def.runtime) return null;
|
||||
const loc = locOf(expr);
|
||||
const local = lowerer.declareHiddenLocal("%constructorReceiver", receiver.type);
|
||||
const value = varRef(local.id, receiver.type, loc);
|
||||
const construct = (current: ClassInfo): IrExpr => {
|
||||
if (current.fields.has("constructor") || current.generic || current.localClass || current.classDecorators) {
|
||||
lowerer.unsupported("SC1090", expr, "instance constructors on generic, local, decorated, or constructor-shadowing classes");
|
||||
}
|
||||
classValueRef(lowerer, current, expr);
|
||||
const result: IrExpr = {
|
||||
kind: "new", className: current.def.name,
|
||||
args: lowerer.completeArgs(expr.arguments ?? [], current.ctorParams, loc, expr),
|
||||
type: { kind: "object", className: current.def.name }, loc,
|
||||
};
|
||||
return upcastTo(lowerer, result, info.def.name);
|
||||
};
|
||||
const visit = (current: ClassInfo): IrExpr => {
|
||||
let result = construct(current);
|
||||
for (const child of current.subclasses) result = {
|
||||
kind: "ternary", cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc },
|
||||
then: visit(child), else_: result, type: receiver.type, loc,
|
||||
};
|
||||
return result;
|
||||
};
|
||||
const bag: IrExpr = { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(value, DYN)], type: DYN, loc };
|
||||
const check = "%class.constructor.check";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === check)) lowerer.liftedFns.push({
|
||||
name: check, params: [{ localId: "bag", name: "bag", type: DYN }], returnType: VOID,
|
||||
locals: [{ id: "bag", name: "bag", type: DYN, mutable: false }], loc,
|
||||
body: [{ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [varRef("bag", DYN, loc),
|
||||
{ kind: "strLit", value: "constructor", type: STRING, loc }], type: BOOL, loc }, then: [
|
||||
{ kind: "runtimeFence", code: "SC1090", message: "construction through a replaced instance constructor is not supported yet", loc },
|
||||
], else_: null, loc }, { kind: "return", value: null, loc }],
|
||||
});
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: local.id, init: receiver, loc },
|
||||
{ kind: "exprStmt", expr: { kind: "call", callee: check, args: [bag], type: VOID, loc }, loc },
|
||||
];
|
||||
return { kind: "seqExpr", stmts, result: visit(info), type: receiver.type, loc };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { funcTypeFromParamShapes, implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
|
||||
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
|
||||
|
||||
/** A method value retains its declaration's identity, not the receiver from
|
||||
* extraction. Its native thunk validates the receiver supplied at call time. */
|
||||
@@ -14,10 +15,19 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
|
||||
const value = methodValue(lowerer, expr, info);
|
||||
if (!value) return null;
|
||||
const loc = locOf(expr);
|
||||
const callback = isClassCallback(lowerer, info, method);
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const local = callback ? lowerer.declareHiddenLocal("%callbackReceiver", receiver.type) : null;
|
||||
const reference = local ? varRef(local.id, receiver.type, loc) : receiver;
|
||||
const finish = (result: IrExpr): IrExpr => {
|
||||
if (!local) return result;
|
||||
const selected = classCallbackValue(lowerer, reference, method, result, loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], result: selected, type: selected.type, loc };
|
||||
};
|
||||
const overrides = [...lowerer.classes.values()].filter((candidate) =>
|
||||
candidate !== info && lowerer.isSubclassOf(candidate.def.name, info.def.name) && candidate.methods.has(method));
|
||||
if (overrides.length === 0) {
|
||||
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.expression), loc }], result: value, type: value.type, loc };
|
||||
return finish({ kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: reference, loc }], result: value, type: value.type, loc });
|
||||
}
|
||||
// Select the declaration when extracting the value. Calling the value
|
||||
// later must not redispatch the method name on a different receiver.
|
||||
@@ -38,7 +48,7 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
|
||||
lowerer.liftedFns.push({ name, params: [{ localId: "this.0", name: "this", type: receiverType }], returnType: value.type,
|
||||
locals: [{ id: "this.0", name: "this", type: receiverType, mutable: false }], body, loc });
|
||||
}
|
||||
return { kind: "call", callee: name, args: [lowerer.lowerExpr(expr.expression)], type: value.type, loc };
|
||||
return finish({ kind: "call", callee: name, args: [reference], type: value.type, loc });
|
||||
}
|
||||
|
||||
function methodValue(lowerer: Lowerer, expr: ts.PropertyAccessExpression, info: ClassInfo): IrExpr | null {
|
||||
|
||||
@@ -300,7 +300,9 @@ export function lowerDynObjectLiteral(
|
||||
const lowerValue = (): IrExpr =>
|
||||
ts.isMethodDeclaration(prop)
|
||||
? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
|
||||
? lowerer.lowerExprExpecting(valueExpr, DYN)
|
||||
: lowerer.lowerExpr(valueExpr as ts.Expression);
|
||||
raw =
|
||||
fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
|
||||
lowerValue();
|
||||
|
||||
@@ -27,6 +27,7 @@ import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
|
||||
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js";
|
||||
import { classCallbackCall, isClassCallback } from "./class-callbacks.js";
|
||||
import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js";
|
||||
import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
|
||||
import { STREAM_API_MEMBERS, lowerStreamMethodCall, lowerStreamModuleCall, lowerStreamStaticCall, streamSidesOf } from "./lower-stream.js";
|
||||
@@ -39,7 +40,7 @@ import { rejectStaticThis } from "./static-this.js";
|
||||
import { fenceNodeModuleMutationCall, lowerRequireCacheKeys } from "./lower-node-module.js";
|
||||
import { defaultAfterUndefined, lowerOptionalArgument, lowerStaticallyUndefinedArgument, positionNumber } from "./optional-arguments.js";
|
||||
import { fenceSymbolFieldCopy } from "./symbol-fields.js";
|
||||
import { lowerClassDataDescriptor } from "./class-descriptors.js";
|
||||
import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
|
||||
export { bodyReadsArguments };
|
||||
@@ -9279,6 +9280,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
|
||||
if (native) return native;
|
||||
}
|
||||
if (target && member === "getOwnPropertyDescriptor" && isDynTypedRefType(target.type)) {
|
||||
const descriptor = lowerClassDescriptorRead(lowerer, call, target);
|
||||
if (descriptor) return descriptor;
|
||||
target = lowerer.coerceToExpected(target, DYN);
|
||||
}
|
||||
// Error.cause owns a live data-property slot shared with its checked
|
||||
@@ -10868,7 +10871,7 @@ 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
|
||||
const probe = mappedReceiver?.kind !== "object" && (lowerer.implicitParamTypes !== null || ts.isNewExpression(access.expression))
|
||||
? tryLowerExpression(lowerer, access.expression) : null;
|
||||
const specializedReceiver = probe?.type.kind === "object" ? probe : null;
|
||||
if (specializedReceiver) mappedReceiver = specializedReceiver.type;
|
||||
@@ -10928,7 +10931,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
// (lowerClassGenericMethodCall has the exactness rules).
|
||||
if (info && !found) {
|
||||
const gfound = findGenericMethodOn(lowerer, info, access.name.text);
|
||||
if (gfound) return lowerClassGenericMethodCall(lowerer, call, access, info, gfound, specializedReceiver ?? undefined);
|
||||
if (gfound) return lowerClassGenericMethodCall(lowerer, call, access, info, gfound, lowerReceiver());
|
||||
}
|
||||
// A FUNC-, nullable-FUNC-, or DYN-typed FIELD in call position:
|
||||
// `this.cb()` — the ctor-assigned callback field (countdown.js's
|
||||
@@ -10990,6 +10993,21 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
}
|
||||
if (!info || !found) return null;
|
||||
const method = access.name.text;
|
||||
if (isClassCallback(lowerer, info, method)) {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerReceiver();
|
||||
const local = lowerer.declareHiddenLocal("%callbackReceiver", receiver.type);
|
||||
const value: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc };
|
||||
const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call);
|
||||
const virtual = lowerer.overrideBelow(info, method);
|
||||
if (virtual) lowerer.noteVirtualEdge(info, method);
|
||||
else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
|
||||
const fallback: IrExpr = virtual
|
||||
? { kind: "virtualCall", className: info.def.name, method, args: [lowerer.upcastTo(value, info.def.name), ...args], type: found.sig.ret, loc }
|
||||
: { kind: "call", callee: `%${found.declarer.def.name}.${method}`, args: [lowerer.upcastTo(value, found.declarer.def.name), ...args], type: found.sig.ret, loc };
|
||||
const result = classCallbackCall(lowerer, value, method, call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), fallback, loc);
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }], result, type: result.type, loc };
|
||||
}
|
||||
if (found.declarer.builtinError) {
|
||||
// The one builtin method: Error.prototype.toString, a runtime
|
||||
// implementation called directly (overriding it is fenced, so no
|
||||
|
||||
@@ -28,6 +28,7 @@ import { 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 { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
|
||||
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
|
||||
if (!ts.isClassExpression(decl)) return false;
|
||||
@@ -1909,6 +1910,20 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
`overriding the builtin Error method '${mName}'`,
|
||||
);
|
||||
}
|
||||
if (overridden && isJsSourceFile(member.getSourceFile())) {
|
||||
// An unannotated JS parameter can use the base's typed ABI.
|
||||
// Defaults stay local to each declaration, so only inherited
|
||||
// required/optional slots participate in this refinement.
|
||||
for (let i = 0; i < shapes.length; i++) {
|
||||
const parameter = member.parameters[i];
|
||||
const inherited = overridden.sig.params[i];
|
||||
if (parameter && inherited && !parameter.type && !parameter.initializer && !parameter.dotDotDotToken &&
|
||||
shapes[i]!.type.kind === "dyn" && inherited.type.kind !== "dyn" &&
|
||||
(inherited.mode === "required" || inherited.mode === "omittable")) {
|
||||
shapes[i] = { ...inherited };
|
||||
}
|
||||
}
|
||||
}
|
||||
// JS overrides may ignore trailing arguments. Retain the base's
|
||||
// checked-value ABI slots as unused parameters, and let shorter
|
||||
// calls fill them with undefined. Typed tails and rest/arguments
|
||||
@@ -3776,7 +3791,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
|
||||
* method `name` — overrideBelow's twin: generic methods have no vtable
|
||||
* slot, so a call that could reach an override compiles only when the
|
||||
* receiver's runtime class is statically exact. */
|
||||
function genericOverrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
export function genericOverrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
|
||||
return info.subclasses.some(
|
||||
(s) => s.genericMethods?.has(name) === true || genericOverrideBelow(lowerer, s, name),
|
||||
);
|
||||
@@ -4675,56 +4690,65 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
|
||||
forward?: IrExpr[],): IrStmt[] {
|
||||
const out: IrStmt[] = [];
|
||||
let superSeen = false;
|
||||
for (const stmt of info.ctor!.body!.statements) {
|
||||
const superCall =
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isCallExpression(stmt.expression) &&
|
||||
stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
? stmt.expression
|
||||
: null;
|
||||
if (!superCall) {
|
||||
out.push(...lowerer.lowerStmts([stmt]));
|
||||
continue;
|
||||
}
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "total");
|
||||
}
|
||||
try {
|
||||
if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls");
|
||||
superSeen = true;
|
||||
const base = superBaseOf(info)!;
|
||||
if (base.builtinEmitter && superCall.arguments.length > 0) {
|
||||
// @types/node admits super({ captureRejections }) — no lowering.
|
||||
lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')");
|
||||
}
|
||||
if (base.builtinStream) {
|
||||
// super(options?) into a runtime stream base: the stream spoke
|
||||
// parses the options and binds overridden underscore methods.
|
||||
out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
const statements = info.ctor!.body!.statements;
|
||||
const entry = { stmts: statements, index: 0, ctx: lowerer.ctx, frame: lowerer.scopes[lowerer.scopes.length - 1]!, out };
|
||||
lowerer.activeStmtLists.push(entry);
|
||||
try {
|
||||
for (let index = 0; index < statements.length; index++) {
|
||||
entry.index = index;
|
||||
const stmt = statements[index]!;
|
||||
const superCall =
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isCallExpression(stmt.expression) &&
|
||||
stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
? stmt.expression
|
||||
: null;
|
||||
if (!superCall) {
|
||||
out.push(...lowerer.lowerStmts([stmt]));
|
||||
continue;
|
||||
}
|
||||
const args = forward !== undefined
|
||||
? forward
|
||||
: base.builtinError
|
||||
? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt)
|
||||
: base.builtinEmitter
|
||||
? []
|
||||
: lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt);
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt)));
|
||||
// super() returns → field initializers → parameter-property
|
||||
// assignments (Node's order, probed) → the rest of the body.
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "failed");
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "total");
|
||||
}
|
||||
try {
|
||||
if (superSeen) lowerer.unsupported("SC1090", stmt, "multiple super() calls");
|
||||
superSeen = true;
|
||||
const base = superBaseOf(info)!;
|
||||
if (base.builtinEmitter && superCall.arguments.length > 0) {
|
||||
// @types/node admits super({ captureRejections }) — no lowering.
|
||||
lowerer.unsupported("SC1090", superCall, "EventEmitter constructor options ('captureRejections')");
|
||||
}
|
||||
if (base.builtinStream) {
|
||||
// super(options?) into a runtime stream base: the stream spoke
|
||||
// parses the options and binds overridden underscore methods.
|
||||
out.push(...lowerStreamSuperCall(lowerer, info, base, superCall.arguments, thisLocal, locOf(stmt), stmt));
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
continue;
|
||||
}
|
||||
const args = forward !== undefined
|
||||
? forward
|
||||
: base.builtinError
|
||||
? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt)
|
||||
: base.builtinEmitter
|
||||
? []
|
||||
: lowerer.completeArgs(superCall.arguments, base.ctorParams, locOf(stmt), stmt);
|
||||
out.push(lowerer.superCallStmt(info, thisLocal, args, locOf(stmt)));
|
||||
// super() returns → field initializers → parameter-property
|
||||
// assignments (Node's order, probed) → the rest of the body.
|
||||
out.push(...lowerer.fieldInitStmts(info, thisLocal));
|
||||
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsFailed++;
|
||||
lowerer.bumpFileStat(locOf(stmt).file, "failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lowerer.activeStmtLists.pop();
|
||||
}
|
||||
if (!superSeen) {
|
||||
// tsc guarantees the call exists somewhere; if it wasn't a top-level
|
||||
@@ -5087,6 +5111,22 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
let selected: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(selected)) selected = selected.expression;
|
||||
if (ts.isConditionalExpression(selected)) {
|
||||
const yes = exactClassOfReceiver(lowerer, selected.whenTrue);
|
||||
const no = exactClassOfReceiver(lowerer, selected.whenFalse);
|
||||
if (yes && no && !yes.classDecorators && !no.classDecorators) {
|
||||
const yesType: IrType = { kind: "object", className: yes.def.name };
|
||||
const noType: IrType = { kind: "object", className: no.def.name };
|
||||
const type: IrType = yes === no ? yesType : { kind: "union", unionId: lowerer.unions.intern([yesType, noType].sort((a, b) => typeKey(a) < typeKey(b) ? -1 : 1)) };
|
||||
return { kind: "ternary", cond: lowerer.lowerCondition(selected.condition),
|
||||
then: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, yes, loc), type),
|
||||
else_: lowerer.coerceInto(expr, lowerProgramClassNew(lowerer, expr, no, loc), type), type, loc };
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "Segmenter" &&
|
||||
lowerer.isStdlibGlobal(expr.expression.expression, "Intl")) {
|
||||
const args = expr.arguments ?? [];
|
||||
|
||||
@@ -49,6 +49,7 @@ import { coerceStringSearchValue, defaultAfterUndefined, lowerStaticallyUndefine
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { classSymbolKeyOf } from "./symbol-fields.js";
|
||||
import { lowerClassMethodValue } from "./class-method-values.js";
|
||||
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
||||
import { lowerGlobalValue } from "./lower-global-value.js";
|
||||
import { lowerClassPrototypeData } from "./class-prototypes.js";
|
||||
|
||||
@@ -667,8 +668,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// every function and constructor global is "function" (the harness's
|
||||
// `typeof queueMicrotask === 'function'` probes). Shadowing locals
|
||||
// have non-stdlib symbols and keep the ordinary path.
|
||||
if (ts.isIdentifier(expr.expression)) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr.expression);
|
||||
let tested = expr.expression;
|
||||
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
|
||||
if (ts.isIdentifier(tested)) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(tested);
|
||||
if (!sym && !lowerer.dynamic) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
const t = lowerer.typeOf(expr.expression);
|
||||
if (
|
||||
@@ -5723,9 +5729,9 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
// type (static `any` — mapTypeOf answers null without --dynamic)
|
||||
// probes the receiver's own lowered world: a dyn value takes the same
|
||||
// write, anything else falls through to the fences.
|
||||
if (receiverIr?.kind === "dyn" || receiverIr === null) {
|
||||
if (receiverIr?.kind === "dyn" || receiverIr?.kind === "object" || receiverIr === null) {
|
||||
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
|
||||
if (obj !== null && obj.type.kind === "dyn") {
|
||||
if (obj !== null && (obj.type.kind === "dyn" || isDynTypedRefType(obj.type))) {
|
||||
const loc = locOf(expr);
|
||||
const litKey = recordKeyLiteralText(target.argumentExpression);
|
||||
let key: IrExpr =
|
||||
@@ -5744,7 +5750,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
`storing '${lowerer.fmt(value.type)}' values in a checked-dynamic object (the value cannot convert into the checked-dynamic tree)`,
|
||||
);
|
||||
}
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [obj, key, value], type: VOID, loc }, loc };
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [lowerer.coerceToExpected(obj, DYN), key, value], type: VOID, loc }, loc };
|
||||
}
|
||||
}
|
||||
if (receiverIr?.kind !== "array") {
|
||||
@@ -6778,7 +6784,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
const reference = scalarSide.type;
|
||||
if ((isDynTypedRefType(reference) || reference.kind === "record" || reference.kind === "array" ||
|
||||
reference.kind === "bytes" || reference.kind === "func" || reference.kind === "regex" || reference.kind === "url" ||
|
||||
reference.kind === "bigint" || reference.kind === "set" || DYN_HANDLE_KINDS.has(reference.kind)) &&
|
||||
reference.kind === "bigint" || reference.kind === "set" || reference.kind === "union" || DYN_HANDLE_KINDS.has(reference.kind)) &&
|
||||
lowerer.dynConvertible(reference)) {
|
||||
const boxed: IrExpr = {
|
||||
kind: "dynFrom", value: scalarSide, type: DYN, loc: scalarSide.loc,
|
||||
@@ -8424,6 +8430,9 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// (name strings are user-writable), so `u instanceof TypeError` keeps
|
||||
// the fence. Reads past the narrow bridge through maybeNarrow's
|
||||
// validated %Error extraction. SEMANTICS.md 67.
|
||||
if (left.type.kind === "dyn" && !target.def.runtime && !target.builtinError && !target.builtinEmitter && !target.builtinStream && !target.localClass) {
|
||||
return classInstanceOf(lowerer, left, target, loc);
|
||||
}
|
||||
if (left.type.kind === "dyn" && target.def.name === "%Error") {
|
||||
return { kind: "dynTest", test: "error", value: left, type: BOOL, loc };
|
||||
}
|
||||
|
||||
@@ -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 { lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js";
|
||||
@@ -5303,6 +5304,8 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
const value = lowerer.lowerExprExpecting(expr.right, target.fieldType);
|
||||
return lowerer.fieldSetStmt(target, value, locOf(expr), expr.left);
|
||||
}
|
||||
const callback = lowerClassCallbackAssign(lowerer, expr);
|
||||
if (callback) return callback;
|
||||
// A write to an ABSTRACT property through an abstract-typed
|
||||
// receiver: the read fence's write twin (the declaration is
|
||||
// erased at runtime — no shared slot exists to write).
|
||||
|
||||
@@ -5773,6 +5773,7 @@ export class Lowerer {
|
||||
const sameFamily =
|
||||
(src.kind === "record" && arm.kind === "record") ||
|
||||
(src.kind === "array" && arm.kind === "array") ||
|
||||
(src.kind === "object" && arm.kind === "object") ||
|
||||
// Tuples already lift into ordinary array slots. Consider that
|
||||
// same conversion when the array is an arm of a union too.
|
||||
(src.kind === "record" && this.shapes.get(src.shapeId)?.tuple === true && arm.kind === "array") ||
|
||||
|
||||
@@ -32,7 +32,7 @@ test("incompatible JS overrides throw at entry without executing the body", asyn
|
||||
class Base { copy(value = 1) { return value; } }
|
||||
class Child extends Base { copy(value) { console.log('must not execute'); return value; } }
|
||||
const child = new Child();
|
||||
try { child.copy(2); } catch (error) { console.log(error.message.includes("overriding method 'copy' with a different signature")); }
|
||||
try { child.copy(2); } catch (error) { console.log(error.message.includes("overriding method 'copy' with a different return type")); }
|
||||
console.log(new Base().copy());
|
||||
`);
|
||||
try {
|
||||
@@ -45,3 +45,26 @@ console.log(new Base().copy());
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("replaced instance constructors refuse before evaluating constructor arguments", async () => {
|
||||
const { dir, entry } = fixture(`
|
||||
function argument() { console.log('must not execute'); return 2; }
|
||||
class Value {
|
||||
constructor(x = 1) { this.x = x; }
|
||||
clone() { return new this.constructor(argument()); }
|
||||
}
|
||||
function replace(value) { value.constructor = Value; }
|
||||
const value = new Value();
|
||||
replace(value);
|
||||
try { value.clone(); } catch (error) { console.log(error.message.includes('replaced instance constructor')); }
|
||||
`);
|
||||
try {
|
||||
const result = await compile(entry, { dynamic: false, outDir: dir, outPath: join(dir, "program"), sanitize: process.env["SCRIPTC_SAN"] === "1" });
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.diagnostics));
|
||||
const run = spawnSync(result.binaryPath, { encoding: "utf8", timeout: 30_000 });
|
||||
expect(run.error).toBeUndefined();
|
||||
expect({ status: run.status, signal: run.signal, stdout: run.stdout, stderr: run.stderr }).toEqual({ status: 0, signal: null, stdout: "true\n", stderr: "" });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11787,6 +11787,42 @@
|
||||
"<repo>/tests/corpus/typed-array-constructor-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/class-callback-replacement.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-callback-replacement.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/class-instance-constructor.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-instance-constructor.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-class-computed-fields.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-class-computed-fields.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-derived-constructor-hoisting.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-derived-constructor-hoisting.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-inherited-parameter-abi.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-inherited-parameter-abi.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-nested-native-descriptors.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-nested-native-descriptors.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
class Signal {
|
||||
constructor(label) { this.label = label; }
|
||||
changed() { console.log('default', this.label); }
|
||||
emit() { this.changed(); }
|
||||
replace(callback) { this.changed = callback; }
|
||||
}
|
||||
class Child extends Signal {
|
||||
changed() { console.log('child', this.label); }
|
||||
parent() { super.changed(); }
|
||||
}
|
||||
const first = new Signal('a'), second = new Signal('b'), child = new Child('c');
|
||||
const original = first.changed;
|
||||
first.emit(); second.emit(); child.emit();
|
||||
let calls = 0;
|
||||
function callback() { calls++; console.log('callback', this.label, calls); }
|
||||
first.replace(callback); child.replace(callback);
|
||||
console.log('identity', first.changed === callback, child.changed === callback);
|
||||
first.emit(); second.emit(); child.emit(); child.parent();
|
||||
original.call(second);
|
||||
console.log('own', Object.hasOwn(first, 'changed'), Object.hasOwn(second, 'changed'));
|
||||
const saved = first.changed;
|
||||
first.replace(() => console.log('replacement'));
|
||||
saved.call(second);
|
||||
first.emit();
|
||||
function dynamic(value) { value.changed(); }
|
||||
dynamic(first); dynamic(child);
|
||||
first.replace(undefined);
|
||||
try { first.emit(); } catch (error) { console.log('invalid', error instanceof TypeError); }
|
||||
function next() { first.replace(() => console.log('next')); return 'argument'; }
|
||||
first.replace((value) => console.log('selected', value));
|
||||
function invoke(value) { value.changed(next()); }
|
||||
invoke(first);
|
||||
first.emit();
|
||||
const fresh = new Signal('fresh');
|
||||
function install() { fresh.replace(() => console.log('installed')); return 1; }
|
||||
function invokeFresh(value) { value.changed(install()); }
|
||||
invokeFresh(fresh);
|
||||
fresh.emit();
|
||||
class PrivateSignal {
|
||||
#changed() { console.log('private'); }
|
||||
emit() { this.#changed(); }
|
||||
}
|
||||
const hidden = new PrivateSignal();
|
||||
function shadowPrivate(value) { value['#changed'] = () => console.log('public'); }
|
||||
shadowPrivate(hidden);
|
||||
hidden.emit();
|
||||
@@ -0,0 +1,24 @@
|
||||
class Value {
|
||||
constructor(x = 1) { this.x = x; }
|
||||
clone() { return new this.constructor(this.x); }
|
||||
reset() { return new this.constructor(); }
|
||||
}
|
||||
class Child extends Value {
|
||||
constructor(x = 2, extra = 3) { super(x); this.extra = extra; }
|
||||
}
|
||||
class Leaf extends Child {}
|
||||
const a = new Value(4), b = new Child(5), c = new Leaf(6);
|
||||
for (const source of [a, b, c]) {
|
||||
const copy = source.clone();
|
||||
console.log(copy !== source, copy.x, copy instanceof Value, copy instanceof Child, copy instanceof Leaf);
|
||||
console.log(source.reset().x);
|
||||
}
|
||||
let order = '';
|
||||
function receiver() { order += 'r'; return b; }
|
||||
function argument() { order += 'a'; return 7; }
|
||||
const selected = new (receiver().constructor)(argument());
|
||||
console.log(order, selected.x, selected instanceof Child);
|
||||
class Other { constructor(x = 9) { this.x = x; } }
|
||||
/** @returns {Value | Other} */
|
||||
function choose(flag) { return new (flag ? Value : Other)(argument()); }
|
||||
console.log(choose(true).x, choose(false).x, order);
|
||||
@@ -0,0 +1,10 @@
|
||||
class Vector { constructor() { this.x = 1; this.y = 2; this.z = 3; } }
|
||||
const value = new Vector();
|
||||
let order = '';
|
||||
function receiver() { order += 'r'; return value; }
|
||||
function key() { order += 'k'; return 'y'; }
|
||||
function number() { order += 'v'; return 8; }
|
||||
receiver()[key()] = number();
|
||||
console.log(order, value.x, value.y, value.z);
|
||||
for (const name of ['x', 'z', 'extra']) value[name] = 4;
|
||||
console.log(value.x, value.z, value.extra);
|
||||
@@ -0,0 +1,14 @@
|
||||
class Base { constructor() { this.x = 1; } }
|
||||
class Child extends Base {
|
||||
constructor() {
|
||||
before();
|
||||
super();
|
||||
let count = 2;
|
||||
after();
|
||||
after();
|
||||
this.x = count;
|
||||
function before() { console.log('before super'); }
|
||||
function after() { count++; console.log('after', count); }
|
||||
}
|
||||
}
|
||||
console.log(new Child().x);
|
||||
@@ -0,0 +1,17 @@
|
||||
class Base {
|
||||
/** @param {boolean} [force] */
|
||||
update(force) { console.log('base', force === undefined, force === true); }
|
||||
forward(force) { this.update(force); }
|
||||
}
|
||||
class Child extends Base {
|
||||
update(force) { console.log('child', force === undefined, force === false); super.update(force); }
|
||||
}
|
||||
const base = new Base(), child = new Child();
|
||||
base.update(); child.update(); child.update(false); child.forward(true);
|
||||
class Defaults {
|
||||
run(value = 5) { console.log('default', value); }
|
||||
}
|
||||
class NoDefault extends Defaults {
|
||||
run(value) { console.log('no default', value === undefined); }
|
||||
}
|
||||
new Defaults().run(); new NoDefault().run(); new NoDefault().run(3);
|
||||
@@ -0,0 +1,18 @@
|
||||
class Position { constructor(x) { this.x = x; } }
|
||||
class Scene {
|
||||
constructor(value) {
|
||||
Object.defineProperties(this, {
|
||||
position: { value, enumerable: true, configurable: true },
|
||||
hidden: { value: new Position(3) },
|
||||
});
|
||||
}
|
||||
}
|
||||
const position = new Position(1);
|
||||
const scene = new Scene(position);
|
||||
console.log(scene.position === position, scene.hidden.x, Object.keys(scene).join(','));
|
||||
position.x = 2;
|
||||
console.log(scene.position.x);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(scene, 'position');
|
||||
console.log(descriptor.value === position, descriptor.writable, descriptor.enumerable, descriptor.configurable);
|
||||
// @ts-expect-error JavaScript typeof permits an unresolvable reference.
|
||||
console.log(typeof scriptcMissingHostGlobal, typeof (scriptcMissingHostGlobal));
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
import { BoxGeometry } from 'three/src/geometries/BoxGeometry.js';
|
||||
import { BufferGeometry } from 'three/src/core/BufferGeometry.js';
|
||||
import { Float32BufferAttribute } from 'three/src/core/BufferAttribute.js';
|
||||
import { Matrix4 } from 'three/src/math/Matrix4.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
const box = new BoxGeometry(2, 4, 6);
|
||||
console.log('box', box.type, box.attributes.position.count, box.index.count, box.groups.length);
|
||||
box.computeBoundingBox();
|
||||
box.computeBoundingSphere();
|
||||
console.log('bounds', box.boundingBox.min.x, box.boundingBox.min.y, box.boundingBox.min.z,
|
||||
box.boundingBox.max.x, box.boundingBox.max.y, box.boundingBox.max.z, box.boundingSphere.radius.toFixed(4));
|
||||
box.translate(1, 2, 3);
|
||||
console.log('translated', box.boundingBox.min.x, box.boundingBox.min.y, box.boundingBox.min.z);
|
||||
const dense = new BoxGeometry(1, 1, 1, 2, 3, 4);
|
||||
console.log('segments', dense.attributes.position.count, dense.index.count, dense.groups[5].materialIndex);
|
||||
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute('position', new Float32BufferAttribute([0, 0, 0, 2, 0, 0, 0, 2, 0], 3));
|
||||
geometry.computeBoundingBox();
|
||||
geometry.computeBoundingSphere();
|
||||
geometry.computeVertexNormals();
|
||||
console.log('triangle', geometry.attributes.position.count, geometry.attributes.normal.getZ(0));
|
||||
console.log('sphere', geometry.boundingSphere.center.x, geometry.boundingSphere.center.y, geometry.boundingSphere.radius.toFixed(4));
|
||||
geometry.applyMatrix4(new Matrix4().makeScale(2, 3, 4));
|
||||
console.log('scaled', geometry.boundingBox.max.x, geometry.boundingBox.max.y, geometry.attributes.normal.getZ(1));
|
||||
geometry.setIndex([0, 1, 2]);
|
||||
console.log('index16', geometry.index.array instanceof Uint16Array, geometry.index.getX(2));
|
||||
geometry.setIndex([0, 65536, 2]);
|
||||
console.log('index32', geometry.index.array instanceof Uint32Array, geometry.index.getX(1));
|
||||
geometry.addGroup(0, 3, 2);
|
||||
geometry.setDrawRange(1, 2);
|
||||
console.log('draw', geometry.groups[0].materialIndex, geometry.drawRange.start, geometry.drawRange.count);
|
||||
geometry.clearGroups();
|
||||
geometry.deleteAttribute('normal');
|
||||
console.log('removed', geometry.groups.length, geometry.hasAttribute('normal'));
|
||||
const point = new Vector3(1, 2, 3), copy = point.clone();
|
||||
copy.x = 4;
|
||||
console.log('clone', copy !== point, point.x, copy.x, copy.y, copy.z);
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
import { Scene } from 'three/src/scenes/Scene.js';
|
||||
import { Object3D } from 'three/src/core/Object3D.js';
|
||||
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera.js';
|
||||
import { Vector3 } from 'three/src/math/Vector3.js';
|
||||
|
||||
const scene = new Scene();
|
||||
scene.position.set(1, 2, 3);
|
||||
const parent = new Object3D(), child = new Object3D();
|
||||
parent.position.set(2, 3, 4);
|
||||
child.position.set(3, 4, 5);
|
||||
parent.add(child);
|
||||
scene.add(parent);
|
||||
scene.updateMatrixWorld();
|
||||
console.log('scene', scene.isScene, scene.children.length, child.parent === parent);
|
||||
console.log('world', child.matrixWorld.elements[12], child.matrixWorld.elements[13], child.matrixWorld.elements[14]);
|
||||
child.rotation.z = Math.PI / 2;
|
||||
console.log('rotation', Number(child.quaternion.z).toFixed(4), Number(child.quaternion.w).toFixed(4));
|
||||
child.quaternion.setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 4);
|
||||
console.log('quaternion', Number(child.rotation.y).toFixed(4));
|
||||
const descriptor = Object.getOwnPropertyDescriptor(child, 'position');
|
||||
console.log('position', descriptor.value === child.position, descriptor.writable, descriptor.enumerable, descriptor.configurable);
|
||||
const names = [];
|
||||
scene.name = 'scene'; parent.name = 'parent'; child.name = 'child';
|
||||
scene.traverse((object) => names.push(object.name));
|
||||
console.log('traverse', names.join(','));
|
||||
parent.remove(child);
|
||||
console.log('removed', parent.children.length, child.parent === null);
|
||||
scene.add(child);
|
||||
scene.updateMatrixWorld();
|
||||
console.log('reparent', child.matrixWorld.elements[12], child.matrixWorld.elements[13], child.matrixWorld.elements[14]);
|
||||
|
||||
const camera = new PerspectiveCamera(70, 1.5, 0.01, 10);
|
||||
camera.position.z = 1;
|
||||
scene.add(camera);
|
||||
scene.updateMatrixWorld(true);
|
||||
console.log('camera', camera.isPerspectiveCamera, camera.position.z, camera.projectionMatrix.elements[11]);
|
||||
console.log('view', camera.matrixWorldInverse.elements[12], camera.matrixWorldInverse.elements[13], camera.matrixWorldInverse.elements[14]);
|
||||
camera.aspect = 2;
|
||||
camera.updateProjectionMatrix();
|
||||
console.log('projection', Number(camera.projectionMatrix.elements[0]).toFixed(4));
|
||||
@@ -21,7 +21,7 @@ function run(command: string, args: string[]) {
|
||||
return { stdout: result.stdout, stderr: result.stderr, status: result.status, signal: result.signal };
|
||||
}
|
||||
|
||||
const cases = ["math", "spatial", "attributes"].flatMap((fixture) =>
|
||||
const cases = ["math", "spatial", "attributes", "geometry", "scene"].flatMap((fixture) =>
|
||||
["native", "wasm32-wasi"].map((target) => ({ fixture, target })));
|
||||
test.for(cases)("published three.js $fixture runs statically through LLVM on $target", async ({ fixture, target }, context) => {
|
||||
const entry = join(import.meta.dirname, `../fixtures/three/${fixture}.mjs`);
|
||||
|
||||
Reference in New Issue
Block a user