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:
Chris Tate
2026-09-29 22:45:39 -05:00
parent 537ea4d28d
commit 66183f93bb
22 changed files with 619 additions and 69 deletions
@@ -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);
+10
View File
@@ -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));
+39
View File
@@ -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);
+40
View File
@@ -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));
+1 -1
View File
@@ -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`);