mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
feat: support static buffer constructors and prototype methods (#591)
* feat: support static buffer constructors and prototype methods - Preserve typed-array constructor identity and shared storage across JavaScript class APIs. - Support chained indexed assignments and mutable prototype methods with native receiver dispatch. - Compile Three.js geometry merging, instancing, and keyframe interpolation without a JavaScript engine. * fix: preserve native collection and dispatch semantics - Preserve typed-array identity in collections and JavaScript argument evaluation. - Keep symbol-backed fields and unreachable methods on their existing static paths. - Restore compiler bootstrapping and deferred class diagnostics. * fix: preserve inferred returns in native compilation - Update recorded return statements in place so native compilation preserves mixed JavaScript return values. * test: update inferred array coverage expectations - Recognize heterogeneous writes supported by shared JavaScript array storage.
This commit is contained in:
@@ -55,7 +55,8 @@ const results = await Promise.all(jobs); // number[] — compiles
|
||||
- `fs.openSync` accepts inline bitwise OR expressions of `fs.constants.O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags remain fenced because their bit values differ by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` compile at direct call sites. On Windows, a numeric open with `O_NOFOLLOW` and `fchmodSync` throw `ENOSYS` because the CRT cannot enforce those contracts; programs requiring them should use a POSIX target.
|
||||
- Numeric `process.exitCode = value` writes in statement position set the implicit exit status; `process.exit()` with no argument uses that status. Reading or resetting `process.exitCode`, and assignments used as values, remain fenced.
|
||||
- `process.getBuiltinModule()` exposes native subsets of `path`, `path/posix`, and `path/win32`, native `os` functions, and main-thread `worker_threads` metadata. Module references and stored functions retain identity. Other modules and exports, including `Worker`, throw `SC2020`. `process.versions` is a shared dictionary containing `node` and `openssl`; other component versions are absent unless defined by the program.
|
||||
- Fixed-length `ArrayBuffer` values support shared typed-array and DataView storage. Resizing and transferring buffers remain unsupported. Numeric typed arrays and Buffer values preserve their element kind and shared storage when passed through `unknown`, including typed arrays nested in records and arrays. Float16 and BigInt typed arrays remain unsupported.
|
||||
- Fixed-length `ArrayBuffer` values support shared typed-array and DataView storage. Resizing and transferring buffers remain unsupported. Numeric typed arrays and Buffer values preserve their element kind and shared storage when passed through `unknown`, including typed arrays nested in records and arrays. Numeric typed-array constructors can be stored and used with `new`, including constructors read from an array's `constructor` property. JavaScript numeric-array parameters and returns preserve typed-array storage. Float16 and BigInt typed arrays remain unsupported.
|
||||
- Named properties and methods on program class prototypes support replacement, inheritance, and deletion. Bare prototype reflection and replacing compiled accessors remain unsupported. Prototype storage on local, generic, mixin, and runtime-provided classes remains unsupported.
|
||||
- `Intl.Segmenter` supports default Unicode grapheme segmentation, segment iteration, and `containing()`. Locale negotiation, word and sentence segmentation, `resolvedOptions()`, and detached method values remain unsupported.
|
||||
- Stored `globalThis` references support identity and probes for absent host capabilities. Most builtin members still require direct global access; reading them through a stored global object can raise `SC2020`.
|
||||
- Map keys and Set elements support numbers and strings by value, and records, class instances, arrays, symbols, and server handles by identity. Unions consisting only of these identity types also compile. Collections with `unknown` slots, including JavaScript's unannotated `new Map()` and `new Set()`, use native checked values: supported primitives compare by value and supported object references retain their identity. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises whose payload requires an adapter cannot cross into those checked slots; `Promise<unknown>` retains its original identity. Other statically typed key forms, mixed scalar/reference unions, nullable key unions, and key conversions that copy an existing reference remain fenced.
|
||||
@@ -86,9 +87,9 @@ Run `scriptc build` on a file to get the full, current list for that program —
|
||||
|
||||
A static-tier program otherwise produces byte-identical stdout and the same exit code as Node. Every known divergence is deliberate and pinned by the differential test suite; these are the ones with real consequences:
|
||||
|
||||
**Typed-array bounds violations still trap.** Ordinary arrays track holes separately from present `undefined` values: missing indexed reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes. Typed-array indexed access retains its separate bounds checks; an invalid typed-array index aborts rather than returning `undefined`.
|
||||
**Typed-array reads retain bounds checks.** Ordinary arrays track holes separately from present `undefined` values: missing indexed reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes. Statically typed numeric reads from typed arrays abort for invalid indexes rather than returning `undefined`. Invalid numeric indexed writes are ignored, preserving the assignment expression's original value.
|
||||
|
||||
**Runtime traps are not catchable.** User `throw` is fully catchable, and runtime failures Node models as exceptions (JSON parse errors, checked-cast failures, fs errors, regex errors) throw real error objects. Remaining hard traps, including typed-array bounds violations, abort the process.
|
||||
**Runtime traps are not catchable.** User `throw` is fully catchable, and runtime failures Node models as exceptions (JSON parse errors, checked-cast failures, fs errors, regex errors) throw real error objects. Remaining hard traps, including invalid statically typed numeric reads from typed arrays, abort the process.
|
||||
|
||||
**Untyped Buffer construction accepts native data.** `Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects. Encoding arguments must be supported literals. Custom `valueOf` hooks on the input object and opaque class or native-handle inputs throw an explicit unsupported-operation error.
|
||||
|
||||
|
||||
@@ -3544,8 +3544,8 @@ export class LlEmitter {
|
||||
case "bytesSet": {
|
||||
// Typed-array element write: same evaluation order as arraySet;
|
||||
// the value is a scalar (the kind-specific inline path coerces
|
||||
// JS-exactly), so no ownership moves. Any invalid index traps — no
|
||||
// append. IrBytesElem is static, so never rediscover it through the
|
||||
// JS-exactly), so no ownership moves. Invalid writes are ignored.
|
||||
// IrBytesElem is static, so never rediscover it through the
|
||||
// generic runtime switch in a hot loop.
|
||||
const arr = this.emitStableReceiver(s.arr, [s.index, s.value]);
|
||||
const integerIndex = this.emitIntegerLoopIndex(s.index);
|
||||
|
||||
@@ -61,7 +61,7 @@ export function emitIntegerLoopIndex(host: LlvmEmitterContext, expr: IrExpr): st
|
||||
return index;
|
||||
}
|
||||
|
||||
export function emitBytesIndex(host: LlvmEmitterContext, receiver: string, index: string, integerIndex = false): string {
|
||||
export function emitBytesIndex(host: LlvmEmitterContext, receiver: string, index: string, integerIndex = false, skipInvalid?: string): string {
|
||||
const B = host.B;
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
@@ -74,11 +74,14 @@ export function emitBytesIndex(host: LlvmEmitterContext, receiver: string, index
|
||||
const valid = B.newLabel("bytes.index.valid");
|
||||
B.condBr(inRange, valid, invalid);
|
||||
B.startBlock(invalid);
|
||||
const indexF64 = B.tmp();
|
||||
B.line(`${indexF64} = uitofp ${host.sizeType} ${index} to double`);
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${indexF64})`);
|
||||
B.terminate("unreachable");
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
const indexF64 = B.tmp();
|
||||
B.line(`${indexF64} = uitofp ${host.sizeType} ${index} to double`);
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${indexF64})`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
B.startBlock(valid);
|
||||
return index;
|
||||
}
|
||||
@@ -106,9 +109,12 @@ export function emitBytesIndex(host: LlvmEmitterContext, receiver: string, index
|
||||
B.condBr(integral, valid, invalid);
|
||||
|
||||
B.startBlock(invalid);
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
B.terminate("unreachable");
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
|
||||
B.startBlock(valid);
|
||||
return idx;
|
||||
@@ -251,7 +257,9 @@ export function emitToUint32(host: LlvmEmitterContext, value: string, expr?: IrE
|
||||
|
||||
export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void {
|
||||
const B = host.B;
|
||||
const idx = host.emitBytesIndex(receiver, index, integerIndex);
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
const done = B.newLabel("bytes.store.done");
|
||||
const idx = emitBytesIndex(host, receiver, index, integerIndex, done);
|
||||
let stored: string | null;
|
||||
if (elem === "u8c") {
|
||||
host.declare(`declare double @scr_bytes_to_u8_clamp(double)`);
|
||||
@@ -268,22 +276,20 @@ export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiv
|
||||
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
|
||||
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i${bits} ${byte}, ptr ${p}, align 1`);
|
||||
return;
|
||||
}
|
||||
if (elem === "f32") {
|
||||
} else if (elem === "f32") {
|
||||
const narrowed = B.tmp();
|
||||
B.line(`${narrowed} = fptrunc double ${value} to float`);
|
||||
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store float ${narrowed}, ptr ${p}, align 1`);
|
||||
return;
|
||||
}
|
||||
if (elem === "f64") {
|
||||
} else if (elem === "f64") {
|
||||
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store double ${value}, ptr ${p}, align 1`);
|
||||
return;
|
||||
} else {
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1`);
|
||||
}
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
export function emitBytesIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind: "bytesIntrinsic" }): LlValue {
|
||||
|
||||
@@ -37,7 +37,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
|
||||
isDynTypedRefType(v.type) ||
|
||||
(v.type.kind === "union" &&
|
||||
(host.unionsById.get(v.type.unionId)?.arms.some(isDynTypedRefType) ?? false));
|
||||
if (e.liveRef || identityRef) {
|
||||
// Bytes already box their shared mutable storage directly. A second
|
||||
// capsule would split identity between checked and native views.
|
||||
if ((e.liveRef && v.type.kind !== "bytes") || identityRef) {
|
||||
if (v.type.kind === "union") {
|
||||
const adapter = host.liveDynUnionRefAdapter(v.type);
|
||||
const boxed = B.tmp();
|
||||
|
||||
@@ -294,8 +294,8 @@ export function liveDynUnionRefAdapter(host: LlvmEmitterContext,
|
||||
}
|
||||
const mutableArms = union.arms
|
||||
.map((arm, tag) => ({ arm, tag }))
|
||||
.filter(({ arm }) => streamTypedRefEligible(arm) || isDynTypedRefType(arm));
|
||||
if (mutableArms.length === 0) {
|
||||
.filter(({ arm }) => arm.kind !== "bytes" && (streamTypedRefEligible(arm) || isDynTypedRefType(arm)));
|
||||
if (mutableArms.length === 0 && !union.arms.some((arm) => arm.kind === "bytes")) {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of immutable union ${key}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
"weakSet.new": "scr_weak_set_new",
|
||||
"dyn.fromEntries": "scr_dyn_from_entries",
|
||||
"bytes.constructor": "scr_bytes_constructor",
|
||||
"bytes.construct": "scr_bytes_construct",
|
||||
"arrayBuffer.new": "scr_array_buffer_new",
|
||||
"ffi.argument": "scr_ffi_argument",
|
||||
"ffi.memoryModule": "scr_ffi_memory_module",
|
||||
|
||||
@@ -5,18 +5,22 @@ 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 { classPrototypeData } from "./class-prototypes.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. */
|
||||
/** Observed method slots and zero-argument notifications can be replaced
|
||||
* through instance properties or their shared prototype chain. */
|
||||
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;
|
||||
return !!found && !found.sig.abstract &&
|
||||
(lowerer.prototypeMethodAccesses.has(name) || found.sig.params.length === 0 && found.sig.ret.kind === "void");
|
||||
}
|
||||
|
||||
function callbackBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
|
||||
function callbackBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc, name: string): IrExpr {
|
||||
const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : null;
|
||||
if (info && lowerer.prototypeMethodAccesses.has(name)) classPrototypeData(lowerer, info, loc);
|
||||
return { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
}
|
||||
@@ -29,34 +33,40 @@ export function lowerClassCallbackAssign(lowerer: Lowerer, expr: ts.BinaryExpres
|
||||
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),
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [callbackBag(lowerer, receiver, loc, access.name.text),
|
||||
{ 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
|
||||
/** Read the 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 },
|
||||
const result: IrExpr = lowerer.prototypeMethodAccesses.has(name) ? { kind: "dynKeyGet", value, key, type: DYN, loc }
|
||||
: { kind: "ternary", cond: { kind: "libCall", fn: "dyn.hasKey", 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 };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: bag.id, init: callbackBag(lowerer, receiver, loc, name), 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 present = lowerer.declareHiddenLocal("%callbackPresent", 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 };
|
||||
const result: IrExpr = { kind: "ternary", cond: varRef(present.id, BOOL, loc), then: call,
|
||||
else_: fallback.type.kind === "void"
|
||||
? { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: fallback, loc }], result: dynUndefinedExpr(loc), type: DYN, loc }
|
||||
: lowerer.coerceToExpected(fallback, DYN), type: DYN, loc };
|
||||
const answer = fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type);
|
||||
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: bag.id, init: callbackBag(lowerer, receiver, loc, name), loc },
|
||||
{ kind: "varDecl", localId: present.id, init: lowerer.prototypeMethodAccesses.has(name)
|
||||
? { kind: "boolLit", value: true, type: BOOL, loc }
|
||||
: { kind: "libCall", fn: "dyn.hasKey", 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 };
|
||||
], result: answer, type: answer.type, loc };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { BOOL, DYN, DYN_CLASS_PROPERTIES as PROPERTY_BAG, STRING, VOID, canConvertToDyn, canDynCheckTo, isClassOwnEnumerableFieldName, isDynTypedRefType, isUnitType, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import { everyStmtList, transformExpr, 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, genericOverrideBelow, upcastTo, type ClassInfo } from "./lower-classes.js";
|
||||
import { accessorCall, classValueRef, findGenericMethodOn, findMethodOn, genericOverrideBelow, upcastTo, type ClassInfo } from "./lower-classes.js";
|
||||
import { classPrototypeData, hasClassPrototypeData } from "./class-prototypes.js";
|
||||
import { errorToStringCall, errorToStringMethod, refreshErrorMethodDispatch } from "./error-methods.js";
|
||||
import { GeneratorDynamicDispatch } from "./generator-dynamic-dispatch.js";
|
||||
@@ -75,6 +75,9 @@ export class ClassDynamicDispatch {
|
||||
private readonly bagInitializers = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>[]>();
|
||||
private readonly typedPropertyBags = new Map<string, IrFunction>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
private constructDispatch: IrFunction | null = null;
|
||||
private readonly constructors = new Map<string, ClassInfo>();
|
||||
private readonly constructed = new Set<string>();
|
||||
private readonly instanceTests = new Map<string, Set<string>>();
|
||||
|
||||
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
|
||||
@@ -111,7 +114,7 @@ export class ClassDynamicDispatch {
|
||||
rewrite.add(fn);
|
||||
break;
|
||||
case "libCall":
|
||||
if (expr.fn === "dyn.keySet" || expr.fn === "dyn.keySetComputed" || (expr.fn === "dyn.iterator" || expr.fn === "dyn.arrayFromIterator") || expr.fn === "dyn.toString" || expr.fn === "dyn.stringConstructor" ||
|
||||
if (expr.fn === "bytes.construct" || expr.fn === "dyn.keySet" || expr.fn === "dyn.keySetComputed" || (expr.fn === "dyn.iterator" || expr.fn === "dyn.arrayFromIterator") || expr.fn === "dyn.toString" || expr.fn === "dyn.stringConstructor" ||
|
||||
expr.fn === "dyn.hasKeyComputed" || expr.fn === "dyn.hasOwnComputed" || expr.fn === "dyn.propertyIsEnumerableComputed") rewrite.add(fn);
|
||||
break;
|
||||
}
|
||||
@@ -147,9 +150,22 @@ export class ClassDynamicDispatch {
|
||||
for (const className of this.boxed) {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
const needsPrototype = (current: ClassInfo): boolean => hasClassPrototypeData(current) || current.subclasses.some(needsPrototype);
|
||||
const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => {
|
||||
const currentReceiver: IrExpr = receiver.type.kind === "object" && receiver.type.className !== current.def.name
|
||||
? { kind: "downcast", value: receiver, type: { kind: "object", className: current.def.name }, loc }
|
||||
: receiver;
|
||||
let result = classPrototypeData(lowerer, current, loc, currentReceiver);
|
||||
for (const child of current.subclasses) {
|
||||
const selected = prototypeFor(child, receiver, loc);
|
||||
if (selected && result) result = { kind: "ternary", cond: { kind: "instanceOf", value: receiver, className: child.def.name, type: BOOL, loc },
|
||||
then: selected, else_: result, type: DYN, loc };
|
||||
}
|
||||
return result;
|
||||
};
|
||||
for (const existingInit of this.bagInitializers.get(className) ?? []) {
|
||||
if (existingInit.value.kind === "dynObjLit" && hasClassPrototypeData(info)) {
|
||||
const prototype = classPrototypeData(lowerer, info, existingInit.loc, existingInit.obj);
|
||||
if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) {
|
||||
const prototype = prototypeFor(info, existingInit.obj, existingInit.loc);
|
||||
if (prototype) {
|
||||
existingInit.value = { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc: existingInit.loc };
|
||||
changed = true;
|
||||
@@ -164,7 +180,7 @@ export class ClassDynamicDispatch {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
|
||||
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className, field: PROPERTY_BAG, type: DYN, loc };
|
||||
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc, receiver) : null;
|
||||
const prototype = needsPrototype(info) ? prototypeFor(info, receiver, loc) : null;
|
||||
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = {
|
||||
kind: "fieldSet", obj: receiver, className, field: PROPERTY_BAG, value: prototype
|
||||
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
|
||||
@@ -237,6 +253,19 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expr.kind === "libCall" && expr.fn === "bytes.construct") {
|
||||
if (!this.constructDispatch) {
|
||||
const loc = expr.loc;
|
||||
const params = [DYN, DYN, STRING].map((type, i) => ({localId: `p.${i}`, name: `p${i}`, type}));
|
||||
this.constructDispatch = { name: "%dyn.class.construct", params, returnType: DYN,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body: [{ kind: "return", value: { ...expr, args: params.map((p) => varRef(p.localId, p.type, loc)) }, loc }], loc };
|
||||
this.generated.add(this.constructDispatch);
|
||||
lowerer.liftedFns.push(this.constructDispatch);
|
||||
changed = true;
|
||||
}
|
||||
return { kind: "call", callee: this.constructDispatch.name, args: expr.args, type: DYN, loc: expr.loc };
|
||||
}
|
||||
const computedRead = expr.kind === "dynKeyGet" && expr.key.kind !== "strLit" ? expr : null;
|
||||
const computedWrite = expr.kind === "libCall" && (expr.fn === "dyn.keySetComputed" || expr.fn === "dyn.keySet" && expr.args[1]?.kind !== "strLit") ? expr : null;
|
||||
const computedProbe = expr.kind === "libCall" && ["dyn.hasKeyComputed", "dyn.hasOwnComputed", "dyn.propertyIsEnumerableComputed"].includes(expr.fn) ? expr : null;
|
||||
@@ -384,11 +413,15 @@ export class ClassDynamicDispatch {
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const receiver = varRef("p.0", DYN, loc);
|
||||
if (isClassCallback(lowerer, info, dispatch.source.method)) {
|
||||
const prototypeMethod = lowerer.prototypeMethodAccesses.has(dispatch.source.method);
|
||||
if (prototypeMethod) classPrototypeData(lowerer, info, loc);
|
||||
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 });
|
||||
}, then: [{ kind: "return", value: { kind: "ternary", cond: prototypeMethod ? { kind: "boolLit", value: true, type: BOOL, loc } : { kind: "libCall", fn: "dyn.hasKey", args: [bag,
|
||||
{ kind: "strLit", value: dispatch.source.method, type: STRING, loc }], type: BOOL, loc }, then: { kind: "dynObjLit", fields: [
|
||||
{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: { kind: "dynKeyGet", value: bag, key: { kind: "strLit", value: dispatch.source.method, type: STRING, loc }, type: DYN, loc } },
|
||||
], type: DYN, loc }, else_: dynUndefinedExpr(loc), type: DYN, loc }, loc }], else_: null, loc });
|
||||
}
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
@@ -412,8 +445,7 @@ export class ClassDynamicDispatch {
|
||||
for (const plan of this.propertyReceivers(lowerer)) {
|
||||
const { info } = plan;
|
||||
if (!isClassOwnEnumerableFieldName(dispatch.name) ||
|
||||
!info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name)) continue;
|
||||
!findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && !info.fields.has(dispatch.name) && !findMethodOn(lowerer, info, `get:${dispatch.name}`) && !findMethodOn(lowerer, info, `set:${dispatch.name}`)) continue;
|
||||
if (dispatch.classes.has(plan.key)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
@@ -441,9 +473,20 @@ export class ClassDynamicDispatch {
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
const memberBody = (name: string): IrStmt[] => dispatch.probe
|
||||
? [{ kind: "return", value: { kind: "boolLit", value: info.fields.has(name) || dispatch.probe === "in", type: BOOL, loc }, loc }]
|
||||
: this.propertyBody(lowerer, { ...dispatch, name }, info, receiver);
|
||||
const memberBody = (name: string): IrStmt[] => {
|
||||
if (!dispatch.probe) return this.propertyBody(lowerer, { ...dispatch, name }, info, receiver);
|
||||
const prototypeMethod = lowerer.prototypeMethodAccesses.has(name);
|
||||
if (info.fields.has(name) || dispatch.probe === "in" && !prototypeMethod) {
|
||||
return [{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }];
|
||||
}
|
||||
if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver);
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
const symbol = name.startsWith("sym:") || name.startsWith("%symbol:");
|
||||
const fn = dispatch.probe === "in" ? "dyn.hasKey" : dispatch.probe === "own" ? "dyn.hasOwn" : "dyn.propertyIsEnumerable";
|
||||
return [{ kind: "return", value: { kind: "libCall", fn: symbol ? `${fn}Computed` : fn,
|
||||
args: [bag, symbol ? varRef("p.key", DYN, loc) : { kind: "strLit", value: name, type: STRING, loc }], type: BOOL, loc }, loc }];
|
||||
};
|
||||
if (dispatch.dynamicKey) {
|
||||
const symbols = new Map<string, IrExpr>();
|
||||
const symbolEntries: [ts.Symbol, string][] = [];
|
||||
@@ -484,6 +527,7 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
const names = new Set([...info.fields.keys()].filter(isClassOwnEnumerableFieldName));
|
||||
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
const methodNames = [...owner.methods.keys()];
|
||||
if (owner.genericMethods) methodNames.push(...[...owner.genericMethods.keys()]);
|
||||
@@ -508,10 +552,51 @@ export class ClassDynamicDispatch {
|
||||
kind: "strEq", left: varRef(dispatch.keyLocal, STRING, loc), right: { kind: "strLit", value: name, type: STRING, loc }, negated: false, type: BOOL, loc,
|
||||
}, then: body, else_: null, loc });
|
||||
}
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: condition, then: branch, else_: null, loc });
|
||||
// Keep each class's key table in a separate function. A single
|
||||
// function containing every boxed class grows quadratically during
|
||||
// LLVM's control-flow optimization for large library workloads.
|
||||
const params = [...dispatch.fn.params];
|
||||
if (dispatch.keyLocal !== "p.key") params.push({ localId: dispatch.keyLocal, name: "key", type: STRING });
|
||||
const helper: IrFunction = {
|
||||
name: `%dyn.class.keys.${lowerer.liftedFns.length}`, params, returnType: dispatch.fn.returnType,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body: [...branch, ...dispatch.fn.body.slice(dispatch.write ? -2 : -1)], loc,
|
||||
};
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
const call: IrExpr = { kind: "call", callee: helper.name,
|
||||
args: params.map((p) => varRef(p.localId, p.type, loc)), type: helper.returnType, loc };
|
||||
const then: IrStmt[] = dispatch.write
|
||||
? [{ kind: "exprStmt", expr: call, loc }, { kind: "return", value: null, loc }]
|
||||
: [{ kind: "return", value: call, loc }];
|
||||
dispatch.fn.body.splice(dispatch.branchIndex, 0, { kind: "if", cond: condition, then, else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (this.constructDispatch) for (const [name, info] of this.constructors) {
|
||||
if (this.constructed.has(name)) continue;
|
||||
this.constructed.add(name);
|
||||
const loc = this.constructDispatch.loc;
|
||||
const callee = varRef("p.0", DYN, loc), input = varRef("p.1", DYN, loc);
|
||||
const args: IrExpr[] = [];
|
||||
let supported = true;
|
||||
for (let i = 0; i < info.ctorParams.length; i++) {
|
||||
const param = info.ctorParams[i]!;
|
||||
if (param.mode !== "required" && param.mode !== "omittable") { supported = false; break; }
|
||||
const value: IrExpr = { kind: "dynKeyGet", value: input, key: { kind: "strLit", value: String(i), type: STRING, loc }, type: DYN, loc };
|
||||
const arg = lowerer.coerceToExpected(value, param.type);
|
||||
if (!typeEquals(arg.type, param.type)) { supported = false; break; }
|
||||
args.push(arg);
|
||||
}
|
||||
if (!supported || !info.decl) continue;
|
||||
const cls = classValueRef(lowerer, info, info.decl);
|
||||
const result: IrExpr = { kind: "new", className: name, args, type: {kind: "object", className: name}, loc };
|
||||
this.constructDispatch.body.unshift({ kind: "if", cond: {kind: "dynScalarEq", left: callee,
|
||||
right: lowerer.coerceToExpected(cls, DYN), type: BOOL, loc}, then: [
|
||||
{ kind: "return", value: lowerer.coerceToExpected(result, DYN), loc },
|
||||
], else_: null, loc });
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -525,11 +610,9 @@ export class ClassDynamicDispatch {
|
||||
if (!initializers) return null;
|
||||
const receiver = varRef("p.0", type, loc);
|
||||
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className: type.className, field: PROPERTY_BAG, type: DYN, loc };
|
||||
const info = lowerer.classes.get(type.className)!;
|
||||
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc, receiver) : null;
|
||||
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = { ...initializers[0]!, obj: receiver, value: prototype
|
||||
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
|
||||
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc };
|
||||
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = { ...initializers[0]!, obj: receiver,
|
||||
value: transformExpr(initializers[0]!.value, { stmt: (stmt) => stmt, expr: (expr) =>
|
||||
expr.kind === "dynCheck" && expr.value.kind === "varRef" && expr.value.localId === "p.0" ? receiver : expr }), loc };
|
||||
const helper: IrFunction = {
|
||||
name: `%class.properties:${type.className}`, params: [{ localId: "p.0", name: "value", type }], returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "value", type, mutable: false }], loc,
|
||||
@@ -604,6 +687,14 @@ export class ClassDynamicDispatch {
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
if (!write) {
|
||||
if (name === "constructor" && !field && !accessor && info.decl && !info.generic && !info.localClass && !info.classDecorators) {
|
||||
this.constructors.set(info.def.name, info);
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
return [{ kind: "return", value: { kind: "ternary", cond: { kind: "libCall", fn: "dyn.hasKey", args: [bag, key], type: BOOL, loc },
|
||||
then: { kind: "dynKeyGet", value: bag, key, type: DYN, loc }, else_: cls, type: DYN, loc }, loc }];
|
||||
}
|
||||
if (name === "toString" && findMethodOn(lowerer, info, name)?.declarer.builtinError) {
|
||||
return [{ kind: "return", value: errorToStringMethod(lowerer, receiver), loc }];
|
||||
}
|
||||
@@ -632,6 +723,19 @@ export class ClassDynamicDispatch {
|
||||
(candidate.methods.has(name) || candidate.genericMethods?.has(name)));
|
||||
overrides.sort((a, b) => lowerer.isSubclassOf(a.def.name, b.def.name) ? -1 : lowerer.isSubclassOf(b.def.name, a.def.name) ? 1 : 0);
|
||||
const body: IrStmt[] = [];
|
||||
if (isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:")) {
|
||||
const prototypeMethod = lowerer.prototypeMethodAccesses.has(name);
|
||||
if (prototypeMethod) classPrototypeData(lowerer, info, loc, receiver);
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
body.push({ kind: "if", cond: { kind: "libCall", fn: "dyn.hasKey", args: [bag, key], type: BOOL, loc },
|
||||
then: [{ kind: "return", value: { kind: "dynKeyGet", value: bag, key, type: DYN, loc }, loc }], else_: null, loc });
|
||||
if (prototypeMethod) {
|
||||
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
|
||||
return body;
|
||||
}
|
||||
}
|
||||
for (const candidate of overrides) {
|
||||
const selected = classMethodValue(lowerer, info.decl, candidate, name, loc);
|
||||
const branch: IrStmt[] = selected && canConvertToDyn(selected.type, getRecord, getUnion)
|
||||
@@ -655,6 +759,13 @@ export class ClassDynamicDispatch {
|
||||
if (boxed.kind === "dynFrom" && mutable(boxed.value.type)) boxed.liveRef = true;
|
||||
return [{ kind: "return", value: boxed, loc }];
|
||||
}
|
||||
if (!field && !accessor && isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:") && !findMethodOn(lowerer, info, `get:${name}`)) {
|
||||
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)], type: DYN, loc };
|
||||
return [{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [bag,
|
||||
{ kind: "strLit", value: name, type: STRING, loc }, varRef("p.1", DYN, loc)], type: VOID, loc }, loc },
|
||||
{ kind: "return", value: null, loc }];
|
||||
}
|
||||
if (!field && !accessor) return [{ kind: "throw", value: {
|
||||
kind: "libCall", fn: "error.new", args: [{
|
||||
kind: "strLit", value: `Cannot set property ${name} of #<${info.def.jsName ?? info.def.name}> which has only a getter`, type: STRING, loc,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BOOL, DYN, STRING, SYMBOL_T, type IrExpr, type IrFunction, type SrcLoc
|
||||
import { varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { exactClassOfReceiver, findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { classMethodValue } from "./class-method-values.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { dynUndefinedExpr } from "./lowerer.js";
|
||||
|
||||
@@ -48,6 +49,21 @@ export function classPrototypeData(lowerer: Lowerer, info: ClassInfo, loc: SrcLo
|
||||
{ kind: "return", value, loc },
|
||||
],
|
||||
};
|
||||
// Materialize only observed method slots. Own declarations stop lookup
|
||||
// at the correct prototype even if an ancestor is replaced later.
|
||||
for (const [method, access] of lowerer.prototypeMethodAccesses) {
|
||||
if (!info.methods.has(method) || method.startsWith("get:") || method.startsWith("set:")) continue;
|
||||
const compiled = classMethodValue(lowerer, access, info, method, locOf(access));
|
||||
if (!compiled) continue;
|
||||
const descriptor: IrExpr = { kind: "dynObjLit", fields: [
|
||||
{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: lowerer.coerceToExpected(compiled, DYN) },
|
||||
{ key: { kind: "strLit", value: "writable", type: STRING, loc }, value: lowerer.coerceToExpected({ kind: "boolLit", value: true, type: BOOL, loc }, DYN) },
|
||||
{ key: { kind: "strLit", value: "configurable", type: STRING, loc }, value: lowerer.coerceToExpected({ kind: "boolLit", value: true, type: BOOL, loc }, DYN) },
|
||||
], type: DYN, loc };
|
||||
const init = helper.body[0]!;
|
||||
if (init.kind === "if") init.then.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.defineProperty", args: [value,
|
||||
lowerer.coerceToExpected({ kind: "strLit", value: method, type: STRING, loc }, DYN), descriptor], type: DYN, loc }, loc });
|
||||
}
|
||||
lowerer.liftedFns.push(helper);
|
||||
}
|
||||
return { kind: "call", callee: info.def.prototypeDataHelper, args: [], type: DYN, loc };
|
||||
@@ -63,8 +79,8 @@ export function isCompiledPrototypeMember(lowerer: Lowerer, info: ClassInfo, nam
|
||||
!!findMethodOn(lowerer, info, `get:${name}`) || !!findMethodOn(lowerer, info, `set:${name}`);
|
||||
}
|
||||
|
||||
/** Compiled methods still live in vtables. Keep prototype reflection and
|
||||
* method replacement fenced until those descriptors have a native view. */
|
||||
/** Named data and method slots have native descriptors. Bare reflection and
|
||||
* accessor replacement still require a complete view of the prototype. */
|
||||
export function lowerClassPrototypeData(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.questionDotToken || expr.name.text !== "prototype") return null;
|
||||
const info = exactClassOfReceiver(lowerer, expr.expression);
|
||||
@@ -73,8 +89,9 @@ export function lowerClassPrototypeData(lowerer: Lowerer, expr: ts.PropertyAcces
|
||||
const name = ts.isPropertyAccessExpression(parent) && parent.expression === expr ? parent.name.text
|
||||
: ts.isElementAccessExpression(parent) && parent.expression === expr && parent.argumentExpression && ts.isStringLiteral(parent.argumentExpression)
|
||||
? parent.argumentExpression.text : null;
|
||||
if (name === null || isCompiledPrototypeMember(lowerer, info, name)) {
|
||||
lowerer.unsupported("SC1090", expr, "class prototype reflection and compiled method replacement (named prototype data properties compile)");
|
||||
const method = name !== null && lowerer.prototypeMethodAccesses.has(name) && findMethodOn(lowerer, info, name);
|
||||
if (name === null || isCompiledPrototypeMember(lowerer, info, name) && !method) {
|
||||
lowerer.unsupported("SC1090", expr, "class prototype reflection and accessor replacement (named prototype data and methods compile)");
|
||||
}
|
||||
const value = classPrototypeData(lowerer, info, locOf(expr), info.localClass ? lowerer.lowerExpr(expr.expression) : undefined);
|
||||
if (value === null) lowerer.unsupported("SC1090", expr, "prototype data on local, generic, mixin, or runtime-provided classes");
|
||||
|
||||
@@ -26,7 +26,7 @@ import { dynStringReceiver, lowerArrayConstructor, lowerArrayFromCall, lowerArra
|
||||
import { lowerBytesStaticCall } from "./containers/bytes.js";
|
||||
import { lowerRegexMethodCall, lowerStringIndexCall, lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall } from "./containers/string-and-regexp.js";
|
||||
import { createRequireSpecOf, lowerChildStreamMethodCall, lowerChildWriterMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerNodeModuleCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
|
||||
import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, stringWrapperToString, templateRawTextOf } from "./lower-exprs.js";
|
||||
import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, stringWrapperToString, symbolFieldInfo, templateRawTextOf } from "./lower-exprs.js";
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
|
||||
@@ -1065,6 +1065,7 @@ function completeFuncValueArgs(
|
||||
// (`.action(() => { throw ... })`). `never` VALUES stay unmapped.
|
||||
if (retTsType.flags & ts.TypeFlags.Never) return VOID;
|
||||
const mappedReturn = lowerer.mapTypeOf(retTsType);
|
||||
if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "array" && mappedReturn.elem.kind === "f64") return DYN;
|
||||
if (isJsSourceFile(decl.getSourceFile()) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "date") return DYN;
|
||||
// JS inference assigns Number to arithmetic over any, although the
|
||||
// runtime operands can both be BigInts. Preserve that native result.
|
||||
@@ -1892,10 +1893,10 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
|
||||
inst.returnType = final;
|
||||
}
|
||||
// The wrap pass: settle every recorded return onto `final`, in place.
|
||||
// Write through the stored union arm: optional-field widening on a
|
||||
// local alias can otherwise introduce a record copy in native builds.
|
||||
for (const e of infer.entries) {
|
||||
if (e.stmt.kind !== "return") continue;
|
||||
// Write through the recorded statement. A narrowed local can acquire a
|
||||
// different record layout and copy it in the native compiler.
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
if (e.stmt.value === null || e.stmt.value === undefined) {
|
||||
@@ -5041,6 +5042,27 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.fenceStaticReadableStreamMember(expr.expression, "call");
|
||||
}
|
||||
|
||||
// Computed native members retain their original receiver and resolve
|
||||
// the function before argument effects, just like dotted calls.
|
||||
if (ts.isElementAccessExpression(expr.expression) && !expr.expression.questionDotToken && !expr.questionDotToken &&
|
||||
!symbolFieldInfo(lowerer, expr.expression)) {
|
||||
const access = expr.expression;
|
||||
const value = tryLowerExpression(lowerer, access.expression);
|
||||
if (value?.type.kind === "dyn" || value?.type.kind === "object" && !lowerer.classes.get(value.type.className)?.def.runtime) {
|
||||
const local = lowerer.declareHiddenLocal("%computedReceiver", value.type);
|
||||
const reference: IrExpr = { kind: "varRef", localId: local.id, type: value.type, loc };
|
||||
const receiver = lowerer.coerceToExpected(reference, DYN);
|
||||
const raw = lowerer.lowerExpr(access.argumentExpression);
|
||||
const key = raw.type.kind === "string" ? raw : lowerer.coerceInto(access.argumentExpression, raw, DYN);
|
||||
const callee: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc };
|
||||
const spread = expr.arguments.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, expr, callee, loc) : null;
|
||||
if (spread && spread.kind !== "dynCall") lowerer.unsupported("SC1090", expr, "computed native method spread arguments");
|
||||
const result: IrExpr = spread !== null ? { ...spread, receiver } : { kind: "dynCall", callee, receiver,
|
||||
calleeName: access.getText(), args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], result, type: DYN, loc };
|
||||
}
|
||||
}
|
||||
|
||||
// The ELEMENT spelling of a primitive method call — `x['toString']()`,
|
||||
// `s['charAt'](0)`: JS resolves it exactly like the dot form, so the
|
||||
// literal-keyed shapes with a static lowering route there before the
|
||||
@@ -6423,9 +6445,11 @@ export function lowerDynDispatchMethodCall(
|
||||
}
|
||||
}
|
||||
}
|
||||
const args = call.arguments.map((arg, i) =>
|
||||
i === 0 && predicate ? lowerer.coerceInto(arg, predicate, DYN) : lowerer.lowerExprExpecting(arg, DYN),
|
||||
);
|
||||
const args = call.arguments.map((arg, i) => {
|
||||
if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN);
|
||||
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg);
|
||||
return undefinedArg ? defaultAfterUndefined(undefinedArg, dynUndefinedExpr(locOf(arg))) : lowerer.lowerExprExpecting(arg, DYN);
|
||||
});
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv,
|
||||
@@ -11080,6 +11104,12 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
lowerUnionObjectMethodCall(lowerer, call, access, mappedReceiver) ??
|
||||
lowerUnionObjectDynFieldCall(lowerer, call, access, mappedReceiver);
|
||||
if (dispatched) return dispatched;
|
||||
const arms = lowerer.unions.get(mappedReceiver.unionId)?.arms;
|
||||
if (isJsSourceFile(call.getSourceFile()) && arms?.every((arm) => arm.kind === "object" &&
|
||||
!lowerer.classes.get(arm.className)?.def.runtime) && lowerer.dynConvertible(mappedReceiver)) {
|
||||
const receiver = lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), DYN);
|
||||
return lowerDynDispatchMethodCall(lowerer, call, access, receiver, false);
|
||||
}
|
||||
}
|
||||
const receiverIr = mappedReceiver?.kind === "object"
|
||||
? mappedReceiver
|
||||
@@ -11189,6 +11219,20 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
|
||||
return finish({ kind: "dynCall", callee, receiver, calleeName: access.getText(), args, type: DYN, loc });
|
||||
}
|
||||
}
|
||||
if (!fieldType && isJsSourceFile(call.getSourceFile()) && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) {
|
||||
const loc = locOf(call);
|
||||
const value = lowerReceiver();
|
||||
const local = lowerer.declareHiddenLocal("%callReceiver", value.type);
|
||||
const reference: IrExpr = { kind: "varRef", localId: local.id, type: value.type, loc };
|
||||
const receiver = lowerer.coerceToExpected(reference, DYN);
|
||||
const callee: IrExpr = { kind: "dynKeyGet", value: receiver,
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc }, type: DYN, loc };
|
||||
const spread = call.arguments.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null;
|
||||
if (spread && spread.kind !== "dynCall") lowerer.unsupported("SC1090", call, "native property call spread arguments");
|
||||
const result: IrExpr = spread !== null ? { ...spread, receiver } : { kind: "dynCall", callee, receiver,
|
||||
calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], result, type: DYN, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!info || !found) return null;
|
||||
@@ -11277,6 +11321,21 @@ function lowerUnionObjectMethodCall(
|
||||
plans.push({ arm, info, found });
|
||||
}
|
||||
const shapes = plans[0]!.found.sig.params;
|
||||
if (plans.some((plan) => isClassCallback(lowerer, plan.info, method))) {
|
||||
const loc = locOf(call);
|
||||
const local = lowerer.declareHiddenLocal("%unionMethodReceiver", DYN);
|
||||
const receiver = varRef(local.id, DYN, loc);
|
||||
const callee: IrExpr = { kind: "dynKeyGet", value: receiver,
|
||||
key: { kind: "strLit", value: method, type: STRING, loc }, type: DYN, loc };
|
||||
const spread = call.arguments.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null;
|
||||
if (spread && spread.kind !== "dynCall") lowerer.unsupported("SC1090", call, "union method spread arguments");
|
||||
const value: IrExpr = spread !== null ? { ...spread, receiver } : { kind: "dynCall", callee, receiver,
|
||||
calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const result = resultT && !isUnitType(resultT) ? lowerer.coerceToExpected(value, resultT) : value;
|
||||
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: local.id,
|
||||
init: lowerer.coerceInto(access.expression, lowerer.lowerExpr(access.expression), DYN), loc }], result, type: result.type, loc };
|
||||
}
|
||||
if (!plans.every((plan) => paramAbisEqual(shapes, plan.found.sig.params))) return null;
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!resultT || isUnitType(resultT)) return null;
|
||||
|
||||
@@ -27,10 +27,11 @@ import { mixinResultBindingClassOf, type MixinInstanceInfo } from "./lower-mixin
|
||||
import { hasLexicalPrivateReference, hasStaticThis, rejectStaticThis } from "./static-this.js";
|
||||
import { lowerUrlNew } from "./lower-url.js";
|
||||
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
|
||||
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
|
||||
const body = member.body;
|
||||
@@ -583,6 +584,13 @@ export interface GenericClassInfo {
|
||||
function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.Expression): IrType {
|
||||
let node = expression;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) {
|
||||
const ret = lowerer.declaredReturnType(node, node);
|
||||
const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms ?? [] : [type];
|
||||
// Inferred arithmetic may return a checked Number or BigInt. Preserve
|
||||
// the callable when its native return ABI differs from the checker slot.
|
||||
if (arms.some((arm) => arm.kind === "func" && !typeEquals(arm.ret, ret))) return DYN;
|
||||
}
|
||||
if (node.kind === ts.SyntaxKind.NullKeyword) return withUnitArm(type, "nullT", lowerer.unions) ?? type;
|
||||
const inferred = lowerer.typeOf(node);
|
||||
for (const arm of inferred.isUnionType() ? ts.constituentTypes(inferred) : [inferred]) {
|
||||
@@ -1980,7 +1988,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
if (
|
||||
implicitMonoFile(decl.getSourceFile()) && !functionLocalClass(decl) &&
|
||||
ts.isIdentifier(member.name) &&
|
||||
!lowerer.virtualJsMethods.has(member) &&
|
||||
!lowerer.virtualJsMethods.has(member) && !lowerer.prototypeMethodAccesses.has(member.name.text) &&
|
||||
inst === undefined && decl.typeParameters === undefined &&
|
||||
!fields.has(member.name.text) &&
|
||||
!lowerer.findMethodOn(base, member.name.text) &&
|
||||
@@ -2000,6 +2008,16 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
}
|
||||
}
|
||||
const { shapes, funcType: ft } = lowerer.lambdaSignature(member);
|
||||
if (isJsSourceFile(member.getSourceFile()) && lowerer.prototypeMethodAccesses.has(mName) &&
|
||||
!lowerer.findMethodOn(base, mName)?.declarer.def.runtime &&
|
||||
shapes.every((p) => p.mode === "required" || p.mode === "omittable")) {
|
||||
for (const shape of shapes) {
|
||||
shape.type = DYN;
|
||||
if (shape.bodyType) shape.bodyType = DYN;
|
||||
}
|
||||
ft.params = shapes.map((shape) => shape.type);
|
||||
if (ft.ret.kind !== "void") ft.ret = DYN;
|
||||
}
|
||||
// JS subclasses can add parameters to a method. Reserve checked
|
||||
// argument slots in the base before constructing its vtable; shorter
|
||||
// bodies ignore them and omitted call arguments become undefined.
|
||||
@@ -4039,7 +4057,23 @@ function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpr
|
||||
* collected subclass graph cannot answer this question yet. The checker
|
||||
* hierarchy identifies the original declarations through aliases as well. */
|
||||
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
if (!files.some(isJsSourceFile)) return;
|
||||
// Record named prototype accesses before collecting method signatures.
|
||||
// Those slots need a callable ABI even when JavaScript would otherwise
|
||||
// specialize each call independently.
|
||||
for (const file of files) ts.walkPreorder(file, (node) => {
|
||||
if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return;
|
||||
const name = ts.isPropertyAccessExpression(node) ? node.name.text
|
||||
: ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
|
||||
if (name === null) return;
|
||||
const prototype = ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "prototype";
|
||||
const assigned = ts.isBinaryExpression(node.parent) && node.parent.left === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
const symbol = !assigned ? undefined : ts.isPropertyAccessExpression(node)
|
||||
? lowerer.checker.getSymbolAtLocation(node.name)
|
||||
: lowerer.checker.getPropertyOfType(lowerer.typeOf(node.expression), name);
|
||||
if (prototype || symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) {
|
||||
lowerer.prototypeMethodAccesses.set(name, node);
|
||||
}
|
||||
});
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
const members = node.members.filter((member): member is ts.MethodDeclaration =>
|
||||
@@ -5154,6 +5188,16 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
|
||||
if (!thisLocal) lowerer.unsupported("SC1080", access);
|
||||
const loc = locOf(call);
|
||||
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
||||
if (lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||
const prototype = classPrototypeData(lowerer, cls.base, loc);
|
||||
if (prototype) {
|
||||
const callee: IrExpr = { kind: "dynKeyGet", value: prototype,
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc }, type: DYN, loc };
|
||||
const value: IrExpr = { kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN),
|
||||
calleeName: access.getText(), args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
return found.sig.ret.kind === "void" ? value : lowerer.coerceToExpected(value, found.sig.ret);
|
||||
}
|
||||
}
|
||||
if (found.declarer.builtinError) {
|
||||
return { kind: "libCall", fn: "error.toString", args: [lowerer.upcastTo(thisRef, "%Error")], type: STRING, loc };
|
||||
}
|
||||
@@ -6317,6 +6361,14 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Checked constructor values carry native typed-array constructor identity.
|
||||
// Other checked values retain an explicit runtime constructor refusal.
|
||||
const callee = tryLowerExpression(lowerer, expr.expression);
|
||||
if (callee?.type.kind === "dyn" && !(expr.arguments ?? []).some(ts.isSpreadElement)) {
|
||||
const args: IrExpr = { kind: "dynArrLit", elems: (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc };
|
||||
return { kind: "libCall", fn: "bytes.construct", args: [callee, args,
|
||||
{ kind: "strLit", value: expr.expression.getText(), type: STRING, loc }], type: DYN, loc };
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr, "constructing values other than classes declared in the program");
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ 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";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import { lowerArrayIsArrayValue } from "./lower-builtin-values.js";
|
||||
import { lowerPerfHooksTypeof } from "./lower-builtins.js";
|
||||
|
||||
@@ -1143,6 +1144,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
) {
|
||||
return primitiveCtorClosure(lowerer, expr.text, loc, jsPrimitiveCtorSelector);
|
||||
}
|
||||
if (Object.values(BYTES_ELEMENT_NAME).includes(expr.text) && lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr))) {
|
||||
return { kind: "libCall", fn: "bytes.constructor", args: [{ kind: "strLit", value: expr.text, type: STRING, loc }], type: DYN, loc };
|
||||
}
|
||||
// The lib fence's IDENTIFIER chokepoint: the real standard library
|
||||
// resolves names the old minimal ambient world never declared
|
||||
// (Symbol, Reflect, Infinity, Date, ...) — and the adopted
|
||||
@@ -1535,6 +1539,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (staticField) return staticField;
|
||||
const prototypeData = lowerClassPrototypeData(lowerer, expr);
|
||||
if (prototypeData) return prototypeData;
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "prototype") {
|
||||
const prototype = lowerClassPrototypeData(lowerer, expr.expression);
|
||||
if (prototype) return { kind: "dynKeyGet", value: prototype,
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc };
|
||||
}
|
||||
// `super.x`: the base chain's GETTER, called directly (super
|
||||
// dispatch is static in JS — never through the dynamic class).
|
||||
// super.method() calls are routed at the call site; a bare super
|
||||
@@ -3600,6 +3609,14 @@ function lowerPromiseThenPresence(
|
||||
lowerer.checker.isTupleType(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(expr.expression)));
|
||||
if (kind === "child" ? !isChildSurfaceMember(lowerer, expr) : !tupleLengthOnArray && !lowerer.isStdlibMember(expr)) return null;
|
||||
const name = expr.name.text;
|
||||
if (kind === "array" && name === "constructor" && isJsSourceFile(expr.getSourceFile())) {
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
if (receiver.type.kind === "dyn") return { kind: "dynKeyGet", value: receiver,
|
||||
key: { kind: "strLit", value: name, type: STRING, loc: locOf(expr) }, type: DYN, loc: locOf(expr) };
|
||||
// Ordinary arrays keep the existing Array builtin identity token.
|
||||
const result: IrExpr = { kind: "strLit", value: "[builtin Array]", type: STRING, loc: locOf(expr) };
|
||||
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: receiver, loc: locOf(expr) }], result, type: STRING, loc: locOf(expr) };
|
||||
}
|
||||
const strictReceiver = (expected: IrType): IrExpr => {
|
||||
const value = lowerer.lowerExpr(expr.expression);
|
||||
if (value.type.kind === "dyn") return lowerer.coerceInto(expr.expression, value, expected);
|
||||
@@ -3832,6 +3849,12 @@ function lowerPromiseThenPresence(
|
||||
}
|
||||
if (kind === "bytes") {
|
||||
const loc = locOf(expr);
|
||||
if (name === "constructor") {
|
||||
const expected = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
const receiver = expected?.kind === "bytes" ? strictReceiver(expected) : lowerer.lowerExpr(expr.expression);
|
||||
return { kind: "dynKeyGet", value: lowerer.coerceInto(expr.expression, receiver, DYN),
|
||||
key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc };
|
||||
}
|
||||
if (name === "length" || name === "byteLength") {
|
||||
const expected = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
let receiver = expected?.kind === "bytes"
|
||||
@@ -4590,11 +4613,17 @@ export function lowerOptionalNumber(
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
const receiver = tryLowerExpression(lowerer, expr.expression);
|
||||
if (receiver?.type.kind === "dyn") {
|
||||
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
|
||||
const optional = hasOptionalChainGuard(expr.expression);
|
||||
return lowerer.maybeNarrow({ kind: "dynKeyGet", value: receiver, key,
|
||||
...(optional ? { optional: true as const } : {}), type: DYN, loc: locOf(expr) }, expr);
|
||||
}
|
||||
if (receiver?.type.kind === "func" && lowerer.dynConvertible(receiver.type)) {
|
||||
const loc = locOf(expr);
|
||||
const fnName = jsFuncNameOf(expr.expression);
|
||||
const boxed: IrExpr = { kind: "dynFrom", value: receiver, type: DYN, loc, ...(fnName !== null ? { fnName } : {}) };
|
||||
const key = lowerRecordPropertyKey(lowerer, lowerer.lowerExpr(expr.argumentExpression), expr.argumentExpression);
|
||||
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
|
||||
return lowerer.maybeNarrow({ kind: "dynKeyGet", key, value: boxed, type: DYN, loc }, expr);
|
||||
}
|
||||
}
|
||||
@@ -5213,6 +5242,9 @@ export function lowerOptionalNumber(
|
||||
* values remain real undefined values, whose JavaScript property spelling
|
||||
* is the string "undefined". */
|
||||
function lowerRecordPropertyKey(lowerer: Lowerer, key: IrExpr, node: ts.Expression): IrExpr {
|
||||
if (key.type.kind === "union" && lowerer.dynConvertible(key.type)) {
|
||||
return { kind: "libCall", fn: "dyn.toStringCoerce", args: [lowerer.coerceInto(node, key, DYN)], type: STRING, loc: key.loc };
|
||||
}
|
||||
if (
|
||||
key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn" ||
|
||||
lowerer.runtimeOptionalWidening(key.type, STRING) !== null
|
||||
@@ -5538,6 +5570,14 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
if (checkedReceiver?.type.kind === "dyn") {
|
||||
return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, checkedReceiver), loc: locOf(expr) };
|
||||
}
|
||||
if (checkedReceiver?.type.kind === "object" && isJsSourceFile(expr.getSourceFile())) {
|
||||
const info = lowerer.classes.get(checkedReceiver.type.className);
|
||||
const key = recordKeyLiteralText(target.argumentExpression) ?? recordKeyTypeLiteralText(lowerer, target.argumentExpression);
|
||||
if (info && key !== null && isClassCallback(lowerer, info, key)) {
|
||||
const receiver = lowerer.coerceInto(target.expression, checkedReceiver, DYN);
|
||||
return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, receiver), loc: locOf(expr) };
|
||||
}
|
||||
}
|
||||
let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(target.expression));
|
||||
if (receiverIr === null || receiverIr.kind === "dyn") {
|
||||
const receiver = tryLowerExpression(lowerer, target.expression);
|
||||
@@ -5615,8 +5655,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
}
|
||||
}
|
||||
// Typed-array element write `b[i] = v` — bytesSet: the value is an f64
|
||||
// the runtime coerces JS-exactly; invalid indices trap (no appends —
|
||||
// typed arrays are fixed-length; JS would ignore the write).
|
||||
// the runtime coerces JS-exactly; invalid indices are ignored.
|
||||
if (receiverIr?.kind === "bytes") {
|
||||
const recv = lowerer.lowerExpr(target.expression);
|
||||
// The write twin: a typed-array .d.ts surface whose VALUE is a
|
||||
@@ -6687,6 +6726,26 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
if (recv?.type.kind === "dyn") {
|
||||
return lowerDynMemberAssignment(lowerer, expr, recv);
|
||||
}
|
||||
if (recv?.type.kind === "array" || recv?.type.kind === "bytes") {
|
||||
const receiver = lowerer.declareHiddenLocal("%setArray", recv.type);
|
||||
const index = lowerer.lowerExpr(expr.left.argumentExpression);
|
||||
if (index.type.kind !== "f64") lowerer.unsupported("SC1090", expr.left.argumentExpression, "indexing with non-number keys");
|
||||
const key = lowerer.declareHiddenLocal("%setIndex", F64);
|
||||
const raw = lowerer.lowerExpr(expr.right);
|
||||
const value = lowerer.declareHiddenLocal("%setValue", raw.type);
|
||||
const result = varRef(value.id, raw.type, loc);
|
||||
const arr = varRef(receiver.id, recv.type, loc);
|
||||
const idx = varRef(key.id, F64, loc);
|
||||
const write: IrStmt = recv.type.kind === "array"
|
||||
? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc)
|
||||
: { kind: "bytesSet", arr, index: idx, value: lowerer.coerceInto(expr.right, result, F64), loc };
|
||||
return { kind: "seqExpr", stmts: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
|
||||
{ kind: "varDecl", localId: key.id, init: index, loc },
|
||||
{ kind: "varDecl", localId: value.id, init: raw, loc },
|
||||
write,
|
||||
], result, type: result.type, loc };
|
||||
}
|
||||
}
|
||||
// `h.k = v` on an ISLAND receiver in VALUE position: the engine
|
||||
// property write (setProp throws the engine's TypeErrors on
|
||||
|
||||
@@ -1660,6 +1660,9 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
|
||||
(uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null)
|
||||
: null;
|
||||
let type = handleT ?? factoryType ?? lowerer.irTypeOf(nameNode);
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && decl.initializer &&
|
||||
ts.isNewExpression(decl.initializer) && lowerer.mapTypeOf(lowerer.typeOf(decl.initializer.expression))?.kind === "dyn" &&
|
||||
!(ts.isIdentifier(decl.initializer.expression) && lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(decl.initializer.expression)))) type = DYN;
|
||||
// Calls through checked function values retain their object
|
||||
// identity, including symbol keys absent from inferred records.
|
||||
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) && type.kind === "record" &&
|
||||
|
||||
@@ -25,7 +25,8 @@ import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js";
|
||||
import { isCompiledPrototypeMember } from "./class-prototypes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { objectFactorySignature } from "./object-factory-new.js";
|
||||
import { lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { isClassCallback, lowerClassCallbackAssign } from "./class-callbacks.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js";
|
||||
@@ -3979,7 +3980,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
|
||||
if (!type) lowerer.badType(decl.name, lowerer.typeOf(decl.name));
|
||||
let settledType: IrType = type;
|
||||
if (!isLet && isJsSourceFile(decl.getSourceFile()) && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isNewExpression(decl.initializer)) {
|
||||
settledType = objectFactorySignature(lowerer, decl.initializer)?.returnType ?? settledType;
|
||||
settledType = init.type.kind === "dyn" ? DYN : objectFactorySignature(lowerer, decl.initializer)?.returnType ?? settledType;
|
||||
}
|
||||
const arithmeticType = decl.initializer ? lowerer.runtimeOptionalArithmeticTypes.get(decl.initializer) : undefined;
|
||||
const isStringArithmeticUnion = (t: IrType): boolean => {
|
||||
@@ -4523,6 +4524,16 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
};
|
||||
}
|
||||
let obj = lowerer.lowerExpr(target.expression);
|
||||
if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") {
|
||||
const info = lowerer.classes.get(obj.type.className);
|
||||
const keyType = ts.isElementAccessExpression(target) ? lowerer.typeOf(target.argumentExpression) : null;
|
||||
const name = ts.isPropertyAccessExpression(target) ? target.name.text
|
||||
: keyType?.isStringLiteralType() ? keyType.value : null;
|
||||
if (info && name !== null && isClassCallback(lowerer, info, name)) {
|
||||
obj = { kind: "call", callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(obj, DYN)], type: DYN, loc };
|
||||
}
|
||||
}
|
||||
if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "func" && lowerer.dynConvertible(obj.type)) {
|
||||
obj = lowerer.coerceToExpected(obj, DYN);
|
||||
}
|
||||
|
||||
@@ -1674,6 +1674,9 @@ export class Lowerer {
|
||||
/** Keyed by program-wide qualified class name (what IR object types carry). */
|
||||
readonly classes = new Map<string, ClassInfo>();
|
||||
readonly classBySymbol = new Map<ts.Symbol, ClassInfo>();
|
||||
/** Named method slots observed before class collection need a callable
|
||||
* ABI and shared prototype descriptors instead of call-site specialization. */
|
||||
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
|
||||
/** Inferred JS methods participating in an override chain keep a vtable
|
||||
* ABI instead of call-site specialization. Filled before class collection. */
|
||||
readonly virtualJsMethods = new Set<ts.MethodDeclaration>();
|
||||
|
||||
@@ -25,6 +25,11 @@ export function jsOpenObjectType(
|
||||
if (!decl || !isJsSourceFile(decl.getSourceFile())) return type;
|
||||
const arms = type.kind === "union" ? unions.get(type.unionId)?.arms : [type];
|
||||
const present = arms?.filter((arm) => arm.kind !== "undefinedT");
|
||||
const parameter = ts.isParameter(decl) ? decl : decl.parent && ts.isParameter(decl.parent) ? decl.parent : null;
|
||||
// Numeric array parameters in JavaScript also accept typed arrays. Keep
|
||||
// their runtime storage at the boundary, including default [] parameters.
|
||||
if (parameter && !parameter.dotDotDotToken && present?.length === 1 &&
|
||||
present[0]!.kind === "array" && present[0]!.elem.kind === "f64") return DYN;
|
||||
if (present?.length !== 1 || present[0]!.kind !== "record") return type;
|
||||
const shape = shapes.get(present[0]!.shapeId);
|
||||
return shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple ? DYN : type;
|
||||
@@ -1873,6 +1878,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// space, so clearTimeout of an Immediate no-ops like Node.
|
||||
if (isStdlibInterface("Immediate")) return F64;
|
||||
if (isStdlibInterface("ArrayBuffer") || isStdlibInterface("PropertyDescriptor") || isStdlibInterface("ProcessVersions")) return DYN;
|
||||
if (Object.values(BYTES_ELEMENT_NAME).some((name) => isStdlibInterface(`${name}Constructor`))) return DYN;
|
||||
if (isStdlibInterface("Uint8Array")) return bytesOf("u8");
|
||||
if (isStdlibInterface("Uint32Array")) return bytesOf("u32");
|
||||
if (isStdlibInterface("Uint8ClampedArray")) return bytesOf("u8c");
|
||||
@@ -2772,7 +2778,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
const ret = jsUnitReturn ? DYN : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx);
|
||||
let ret = jsUnitReturn ? DYN : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx);
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array" && ret.elem.kind === "f64") ret = DYN;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
|
||||
@@ -2022,6 +2022,8 @@ export type IrLibFn =
|
||||
| "weakMap.new"
|
||||
| "weakSet.new"
|
||||
| "dyn.fromEntries"
|
||||
| "bytes.constructor"
|
||||
| "bytes.construct"
|
||||
| "arrayBuffer.new"
|
||||
| "arrayBuffer.is"
|
||||
| "arrayBuffer.isView"
|
||||
@@ -7264,6 +7266,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"weakMap.new",
|
||||
"weakSet.new",
|
||||
"dyn.fromEntries",
|
||||
"bytes.constructor",
|
||||
"bytes.construct",
|
||||
"arrayBuffer.new",
|
||||
"arrayBuffer.byteLengthGetter",
|
||||
"arrayBuffer.viewU8C",
|
||||
|
||||
@@ -1149,6 +1149,8 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"weakMap.new": { argTypes: [DYN], result: DYN },
|
||||
"weakSet.new": { argTypes: [DYN], result: DYN },
|
||||
"dyn.fromEntries": { argTypes: [DYN], result: DYN },
|
||||
"bytes.constructor": { argTypes: [STRING], result: DYN },
|
||||
"bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN },
|
||||
"arrayBuffer.new": { argTypes: [DYN], result: DYN },
|
||||
"ffi.argument": { argTypes: [DYN, STRING], result: DYN },
|
||||
"ffi.memoryModule": { argTypes: [DYN], result: DYN },
|
||||
|
||||
@@ -8938,6 +8938,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/class-property-call-values.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-property-call-values.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/class-prototype-data.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-prototype-data.js"
|
||||
@@ -9305,6 +9311,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/indexed-assignment-values.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/indexed-assignment-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/instanceof-class-record-union.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/instanceof-class-record-union.ts"
|
||||
@@ -9992,6 +10004,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/prototype-method-overrides.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/prototype-method-overrides.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/prototype-named-data-records.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/prototype-named-data-records.ts"
|
||||
@@ -10250,6 +10268,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-array-constructor-alias.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-constructor-alias.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-array-constructor-identity.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-constructor-identity.js"
|
||||
@@ -10262,6 +10286,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-array-constructor-values.mjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-constructor-values.mjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-array-dynamic-methods.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-array-dynamic-methods.js"
|
||||
@@ -10280,6 +10310,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-prototype-methods.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-prototype-methods.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/typed-rest-callables.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/typed-rest-callables.ts"
|
||||
|
||||
@@ -172,9 +172,8 @@ double scr_bytes_byte_len(const ScrBytes *b) {
|
||||
}
|
||||
|
||||
/* ── element access ────────────────────────────────────────────────────
|
||||
* JS reads undefined and IGNORES writes out of bounds on typed arrays;
|
||||
* both are unrepresentable here, so any invalid index traps — the array
|
||||
* runtime's exact discipline (SEMANTICS.md). */
|
||||
* Invalid writes are ignored. Typed numeric reads cannot represent
|
||||
* undefined, so invalid reads trap; checked reads return undefined. */
|
||||
|
||||
static size_t scr_bytes_check_index(const ScrBytes *b, double i) {
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) {
|
||||
@@ -241,7 +240,8 @@ double scr_bytes_get(const ScrBytes *b, double i) {
|
||||
}
|
||||
|
||||
void scr_bytes_set(ScrBytes *b, double i, double v) {
|
||||
size_t idx = scr_bytes_check_index(b, i);
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return;
|
||||
size_t idx = (size_t)i;
|
||||
switch (b->elem) {
|
||||
case SCR_BYTES_U8: case SCR_BYTES_I8:
|
||||
b->data[idx] = (uint8_t)scr_bytes_to_u32(v);
|
||||
|
||||
@@ -1030,7 +1030,7 @@ static ScrDyn *scr_dyn_invoke_impl(
|
||||
ScrDyn *needle = argc > 0 ? args[0] : scr_dyn_undefined();
|
||||
double from = dyn_name_is(method, "lastIndexOf") ? (double)len - 1 : 0;
|
||||
if (argc > 1) {
|
||||
from = dyn_index_arg(args, argc, 1, from, what);
|
||||
from = dyn_index_arg(args, argc, 1, 0, what);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
}
|
||||
if (!dyn_arr_length_unchanged(recv, len)) return NULL;
|
||||
|
||||
@@ -1656,6 +1656,74 @@ ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d) {
|
||||
return scr_bytes_retain(d->v.bytes);
|
||||
}
|
||||
|
||||
/* Numeric typed-array constructor values share native function identity.
|
||||
* The cache owns one reference until runtime cleanup; arbitrary strings or
|
||||
* functions cannot impersonate one of these constructors. */
|
||||
static SCR_TL ScrDyn *scr_bytes_ctors[9];
|
||||
static void scr_bytes_ctors_cleanup(void) {
|
||||
for (size_t i = 0; i < 9; i++) {
|
||||
scr_dyn_release(scr_bytes_ctors[i]);
|
||||
scr_bytes_ctors[i] = NULL;
|
||||
}
|
||||
}
|
||||
static ScrDyn *scr_bytes_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
|
||||
(void)args; (void)argc;
|
||||
for (size_t i = 0; i < 9; i++) {
|
||||
if (!scr_bytes_ctors[i] || scr_bytes_ctors[i]->v.fn.clo != closure) continue;
|
||||
ScrJsonBuf message;
|
||||
scr_jb_init(&message);
|
||||
scr_jb_puts(&message, "Constructor ");
|
||||
scr_jb_puts(&message, scr_bytes_elem_name((ScrBytesElem)i));
|
||||
scr_jb_puts(&message, " requires 'new'");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
|
||||
return NULL;
|
||||
}
|
||||
scr_trap("scriptc: invalid typed array constructor\n");
|
||||
}
|
||||
static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function);
|
||||
static ScrDyn *scr_bytes_constructor_for(ScrBytesElem elem) {
|
||||
if (!scr_bytes_ctors[elem]) {
|
||||
bool initialized = false;
|
||||
for (size_t i = 0; i < 9; i++) if (scr_bytes_ctors[i]) initialized = true;
|
||||
if (!initialized) scr_atexit(scr_bytes_ctors_cleanup);
|
||||
scr_bytes_ctors[elem] = scr_dyn_new_func(scr_closure_new(NULL, 0),
|
||||
scr_bytes_ctor_call, 3, "typed-array-constructor", scr_bytes_elem_name(elem));
|
||||
ScrDyn *properties = scr_dyn_fn_properties(scr_bytes_ctors[elem]);
|
||||
scr_dyn_obj_set(properties, "BYTES_PER_ELEMENT", 17, scr_dyn_new_num((double)scr_bytes_elem_size(elem)));
|
||||
ScrDynEntry *entry = &properties->v.obj.entries[properties->v.obj.len - 1];
|
||||
entry->writable = entry->enumerable = entry->configurable = false;
|
||||
scr_dyn_release(properties);
|
||||
}
|
||||
return scr_dyn_retain(scr_bytes_ctors[elem]);
|
||||
}
|
||||
ScrDyn *scr_bytes_constructor(const ScrStr *name) {
|
||||
for (size_t i = 0; i < 9; i++) {
|
||||
const char *candidate = scr_bytes_elem_name((ScrBytesElem)i);
|
||||
if (strlen(candidate) == name->len && memcmp(candidate, name->data, name->len) == 0)
|
||||
return scr_bytes_constructor_for((ScrBytesElem)i);
|
||||
}
|
||||
scr_trap("scriptc: invalid typed array constructor name\n");
|
||||
}
|
||||
ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) {
|
||||
for (size_t i = 0; i < 9; i++) {
|
||||
if (callee->kind != SCR_DYN_FUNC || !scr_bytes_ctors[i] || callee->v.fn.clo != scr_bytes_ctors[i]->v.fn.clo) continue;
|
||||
const ScrDyn *input = args->v.arr.len > 0 ? args->v.arr.items[0] : scr_dyn_undefined();
|
||||
const ScrDyn *offset = args->v.arr.len > 1 ? args->v.arr.items[1] : scr_dyn_undefined();
|
||||
const ScrDyn *length = args->v.arr.len > 2 ? args->v.arr.items[2] : scr_dyn_undefined();
|
||||
ScrBytes *bytes = scr_array_buffer_view((ScrBytesElem)i, input, offset, length);
|
||||
if (!bytes) return NULL;
|
||||
ScrDyn *result = scr_dyn_new_bytes(bytes);
|
||||
scr_bytes_release(bytes);
|
||||
return result;
|
||||
}
|
||||
ScrJsonBuf message;
|
||||
scr_jb_init(&message);
|
||||
scr_jb_write(&message, what->data, what->len);
|
||||
scr_jb_puts(&message, " is not a supported native constructor");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Shared view metadata and indexed reads for both compiler backends. */
|
||||
ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) {
|
||||
ScrBytes *bytes = value->v.bytes;
|
||||
@@ -1664,7 +1732,8 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) {
|
||||
if (key->len == 10 && memcmp(key->data, "byteOffset", 10) == 0) return scr_dyn_new_num(scr_bytes_byte_offset(bytes));
|
||||
if (key->len == 6 && memcmp(key->data, "buffer", 6) == 0) return scr_array_buffer_from_bytes(bytes);
|
||||
if (key->len == 11 && memcmp(key->data, "constructor", 11) == 0) {
|
||||
/* Match the compiler's opaque JS builtin identity values. */
|
||||
if (!value->buffer) return scr_bytes_constructor_for(bytes->elem);
|
||||
/* Buffer retains its separate builtin identity. */
|
||||
char token[64];
|
||||
int length = snprintf(token, sizeof token, "[builtin %s]", value->buffer ? "Buffer" : scr_bytes_elem_name(bytes->elem));
|
||||
ScrStr *name = scr_str_new(token, (size_t)length);
|
||||
|
||||
@@ -3730,6 +3730,8 @@ void scr_dyn_proxy_unsupported(const char *operation);
|
||||
/* Retains the native view, preserving identity and its shared backing
|
||||
* allocation across the checked-dynamic boundary. Borrows b. */
|
||||
ScrDyn *scr_dyn_new_bytes(const ScrBytes *b);
|
||||
ScrDyn *scr_bytes_constructor(const ScrStr *name);
|
||||
ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what);
|
||||
ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key);
|
||||
/* The Buffer-flavored twin (stream chunks): string coercion/toString
|
||||
* decode utf8 instead of joining elements. */
|
||||
@@ -5437,8 +5439,8 @@ double scr_bit_not(double a);
|
||||
* construction (owners point at nothing): never part of a cycle, no
|
||||
* trace. Element reads widen to double; writes coerce JS-exactly (ToUint8
|
||||
* / ToUint32 modular truncation, double→float rounding for f32).
|
||||
* Out-of-bounds ELEMENT access traps like arrays (JS returns undefined /
|
||||
* ignores the write — documented divergence); the Node-shaped operations
|
||||
* Out-of-bounds typed ELEMENT reads trap like arrays; invalid writes are
|
||||
* ignored as in JavaScript. The Node-shaped operations
|
||||
* (construction lengths, set(), the read/write numeric families, the
|
||||
* DataView constructor and getters) THROW catchable RangeErrors exactly
|
||||
* where Node throws. */
|
||||
@@ -5594,9 +5596,8 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind,
|
||||
* with the getters' one RangeError. */
|
||||
void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le);
|
||||
|
||||
/* Element read/write. Any invalid index — negative, fractional, NaN, or
|
||||
* out of bounds — TRAPS like the array runtime (SEMANTICS.md documents
|
||||
* the divergence from JS's undefined-read/ignored-write). Writes coerce
|
||||
/* Element read/write. Invalid typed reads trap; invalid writes are ignored.
|
||||
* Checked reads preserve JavaScript's undefined result. Valid writes coerce
|
||||
* JS-exactly: u8/u32 by modular truncation (NaN/±Infinity → 0, truncate
|
||||
* toward zero, wrap mod 2^8/2^32), f32 by double→float rounding. */
|
||||
double scr_bytes_get(const ScrBytes *b, double i);
|
||||
|
||||
@@ -62,13 +62,11 @@ test("bytes runtime: coercions, encodings, zlib, fs, RC", async () => {
|
||||
expect(stderr.trim().split("\n").pop()).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
});
|
||||
|
||||
// JS reads undefined / ignores writes out of bounds on typed arrays; both
|
||||
// are unrepresentable, so the runtime traps (documented divergence, the
|
||||
// array runtime's exact discipline).
|
||||
// Statically typed numeric reads cannot represent undefined and still trap.
|
||||
// Invalid writes are ignored and covered by the runtime assertions above.
|
||||
test.each([
|
||||
["--crash-get-oob", "typed array index 1 out of bounds (length 1)"],
|
||||
["--crash-get-frac", "typed array index 0.5 out of bounds (length 1)"],
|
||||
["--crash-set-oob", "typed array index 1 out of bounds (length 1)"],
|
||||
])("trap aborts (%s)", async (mode, message) => {
|
||||
const err = await execFileAsync(bin, [mode]).then(
|
||||
() => {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* <scratch-dir> run all assertions; prints "N/N cases passed"
|
||||
* --crash-get-oob element read past the end → RangeError + abort()
|
||||
* --crash-get-frac fractional element index → RangeError + abort()
|
||||
* --crash-set-oob element write past the end → RangeError + abort()
|
||||
*
|
||||
* The coercion matrix mirrors Node exactly (verified by hand and by the
|
||||
* differential corpus): ToUint8/ToUint32 modular truncation on writes,
|
||||
@@ -61,6 +60,17 @@ static void test_construction(void) {
|
||||
check_f64(scr_bytes_get(b, 0), 0, "zero-filled");
|
||||
scr_bytes_release(b);
|
||||
|
||||
b = scr_bytes_new(SCR_BYTES_U8, 1);
|
||||
scr_bytes_set(b, 0, 3);
|
||||
const double invalid_indices[] = { -1, 0.5, 1, NAN, INFINITY, -INFINITY };
|
||||
for (size_t i = 0; i < sizeof invalid_indices / sizeof invalid_indices[0]; i++) {
|
||||
scr_bytes_set(b, invalid_indices[i], 7);
|
||||
check_f64(scr_bytes_get(b, 0), 3, "invalid writes preserve existing elements");
|
||||
check_f64(scr_bytes_len(b), 1, "invalid writes do not grow typed arrays");
|
||||
check(!scr_exc_pending(), "invalid writes do not throw");
|
||||
}
|
||||
scr_bytes_release(b);
|
||||
|
||||
/* ToIndex: 3.5 truncates to 3, NaN is 0 — no throw (Node-exact). */
|
||||
b = scr_bytes_new(SCR_BYTES_U8, 3.5);
|
||||
check_f64(scr_bytes_len(b), 3, "new(3.5) truncates to 3");
|
||||
@@ -376,8 +386,6 @@ int main(int argc, char **argv) {
|
||||
scr_bytes_get(b, 1);
|
||||
} else if (strcmp(argv[1], "--crash-get-frac") == 0) {
|
||||
scr_bytes_get(b, 0.5);
|
||||
} else if (strcmp(argv[1], "--crash-set-oob") == 0) {
|
||||
scr_bytes_set(b, 1, 7); /* JS ignores; we trap (no appends either) */
|
||||
} else {
|
||||
fprintf(stderr, "unknown mode %s\n", argv[1]);
|
||||
return 2;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
class Table {
|
||||
constructor() { this.initialize(); }
|
||||
initialize() { this.entries = {}; }
|
||||
write(key, value) { this.entries[key] = value; }
|
||||
read(key) { return this.entries[key]; }
|
||||
install() { this.compute = function(value) { return this.read('factor') * value; }; }
|
||||
}
|
||||
const table = new Table();
|
||||
table.write('factor', 3);
|
||||
table.install();
|
||||
console.log(table.compute(4));
|
||||
const key = Math.random() < 2 ? 'factor' : 3;
|
||||
console.log(table.read(key));
|
||||
table.write(3, 7);
|
||||
console.log(table.read(3));
|
||||
class First { result(value) { return value + 1; } }
|
||||
class Second { result(value) { return value * 2; } }
|
||||
const chosen = Math.random() < 2 ? new First() : new Second();
|
||||
console.log(chosen.result(3));
|
||||
@@ -0,0 +1,18 @@
|
||||
const bytes = new Uint8Array(4);
|
||||
const numbers = [0, 0, 0, 0];
|
||||
let trace = "";
|
||||
function target(): Uint8Array { trace += "receiver "; return bytes; }
|
||||
function index(n: number): number { trace += "index" + n + " "; return n; }
|
||||
function value(): number { trace += "value "; return 257.5; }
|
||||
console.log(target()[index(0)] = target()[index(1)] = value());
|
||||
console.log(bytes[0], bytes[1], trace);
|
||||
console.log(numbers[0] = numbers[1] = 9, numbers.join(","));
|
||||
console.log(bytes[99] = -7.5, bytes.length);
|
||||
console.log(bytes[-1] = 12.5, bytes[0.5] = 12.5, bytes[NaN] = 12.5, bytes[Infinity] = 12.5, bytes[-Infinity] = 12.5);
|
||||
const floats = new Float32Array(1);
|
||||
const assigned = floats[0] = 1 / 3;
|
||||
console.log(assigned === 1 / 3, floats[0] === Math.fround(1 / 3));
|
||||
let current = new Uint8Array(1);
|
||||
const original = current;
|
||||
function replace(): number { current = new Uint8Array(1); return 6; }
|
||||
console.log(current[0] = replace(), original[0], current[0]);
|
||||
@@ -1,5 +1,4 @@
|
||||
// @deferred-fences: 1
|
||||
// The incompatible string write is intentionally fenced; toArray stays uncalled.
|
||||
// Inferred JavaScript array storage also admits heterogeneous writes in unused methods.
|
||||
class Coordinates {
|
||||
constructor() { this.value = 3; }
|
||||
/** @param {number[]} values */
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
class Base {
|
||||
constructor(value = 2) { this.value = value; }
|
||||
scale(n) { return this.value * n; }
|
||||
}
|
||||
class Child extends Base {}
|
||||
class Own extends Base { scale(n) { return this.value + n; } }
|
||||
const b = new Base(), c = new Child(), o = new Own();
|
||||
const original = Base.prototype.scale;
|
||||
function presence(value, key) {
|
||||
console.log(key in value, Object.hasOwn(value, key), Object.prototype.propertyIsEnumerable.call(value, key));
|
||||
}
|
||||
presence(b, 'scale');
|
||||
console.log(b.scale(3), c.scale(3), o.scale(3), c.scale === original);
|
||||
Base.prototype.scale = function(n) { return this.value * n + 1; };
|
||||
console.log(b.scale(3), c.scale(3), o.scale(3));
|
||||
Child.prototype.scale = undefined;
|
||||
console.log(typeof c.scale, typeof b.scale, typeof o.scale);
|
||||
try { c.scale(3); } catch (error) { console.log(error.name); }
|
||||
Child.prototype.scale = function(n) { return this.value + n + 10; };
|
||||
console.log(c.scale(3), b.scale(3), o.scale(3));
|
||||
Base.prototype.scale = original;
|
||||
class MessageError extends Error {
|
||||
toString() { return this.message; }
|
||||
}
|
||||
const error = new MessageError('native error override');
|
||||
const errorMethod = MessageError.prototype.toString;
|
||||
console.log(error.toString(), errorMethod.call(error));
|
||||
// @ts-expect-error JavaScript permits deleting a prototype method.
|
||||
delete Base.prototype.scale;
|
||||
presence(b, 'scale');
|
||||
console.log(typeof b.scale, typeof c.scale);
|
||||
try { throughBase(b); } catch (error) { console.log(error.name); }
|
||||
Base.prototype.scale = original;
|
||||
console.log(b.scale(3), c.scale(3));
|
||||
function throughBase(value) { return value.scale(4); }
|
||||
console.log(throughBase(b), throughBase(c), throughBase(o));
|
||||
function replace() { Base.prototype.scale = function(n) { return n + 100; }; return 5; }
|
||||
console.log(b.scale(replace()), b.scale(5));
|
||||
const key = 'scale';
|
||||
console.log(c[key](2), o[key](2));
|
||||
let reads = 0;
|
||||
function receiver() { reads++; return c; }
|
||||
console.log(receiver()[key](3), reads);
|
||||
const computed = c[key];
|
||||
console.log(computed.call(c, 1));
|
||||
c[key] = function(n) { return this.value + n + 20; };
|
||||
presence(c, key);
|
||||
console.log(c.scale(1), throughBase(c));
|
||||
// @ts-expect-error JavaScript permits deleting a method override.
|
||||
delete c[key];
|
||||
presence(c, key);
|
||||
console.log(c.scale(1));
|
||||
// @ts-expect-error JavaScript permits deleting a prototype method.
|
||||
delete Child.prototype.scale;
|
||||
console.log(c.scale(2), b.scale(2));
|
||||
class Forward extends Base { scale(n) { return super.scale(n) + 10; } }
|
||||
const f = new Forward();
|
||||
console.log(f.scale(2));
|
||||
Base.prototype.scale = original;
|
||||
console.log(f.scale(2));
|
||||
Base.prototype.scale = undefined;
|
||||
try { f.scale(2); } catch (error) { console.log(error.name); }
|
||||
Base.prototype.scale = original;
|
||||
@@ -0,0 +1,10 @@
|
||||
const Constructor = Float32Array;
|
||||
const values = new Constructor([1, 2, 3]);
|
||||
console.log(values.length, values[1], typeof Constructor, values.constructor === Constructor);
|
||||
const alias = values.subarray(1);
|
||||
const unknown: unknown = alias;
|
||||
console.log(unknown === alias);
|
||||
const buffer = new ArrayBuffer(16);
|
||||
const view = new Constructor(buffer, 4, 2);
|
||||
view[0] = 7;
|
||||
console.log(new Float32Array(buffer)[1], view.byteOffset, view.buffer === buffer);
|
||||
@@ -0,0 +1,28 @@
|
||||
function callWithoutNew(value) { return value(1); }
|
||||
const constructors = [Uint8Array, Uint8ClampedArray, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array];
|
||||
for (const C of constructors) {
|
||||
const value = new C([1, 257.5, -2]);
|
||||
const Saved = value.constructor;
|
||||
const copy = new Saved(value);
|
||||
copy[0] = 8;
|
||||
console.log(typeof C, C.name, C.length, C.BYTES_PER_ELEMENT, C === Saved, value[0], copy[0], copy[1], copy[2]);
|
||||
const view = new Saved(value.buffer, value.byteOffset, value.length);
|
||||
view[0] = 9;
|
||||
console.log(view.buffer === value.buffer, value[0], new Saved().length, new Saved(2).length);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(C, 'BYTES_PER_ELEMENT');
|
||||
console.log(descriptor.value, descriptor.writable, descriptor.enumerable, descriptor.configurable);
|
||||
try { new Saved(-1); } catch (error) { console.log(error.name); }
|
||||
try { callWithoutNew(C); } catch (error) { console.log(error.name, error.message); }
|
||||
}
|
||||
class Writer {
|
||||
/** @param {number[]} [output=[]] @returns {number[]} */
|
||||
write(output = []) { output[0] = 257.5; output[1] = -2; return output; }
|
||||
}
|
||||
const writer = new Writer();
|
||||
const target = new Uint8Array(2);
|
||||
console.log(writer.write(target) === target, target[0], target[1], writer.write().length);
|
||||
let trace = '';
|
||||
function constructor() { trace += 'constructor '; return Float32Array; }
|
||||
function argument() { trace += 'argument '; return 2; }
|
||||
const made = new (constructor())(argument());
|
||||
console.log(made.length, trace);
|
||||
@@ -0,0 +1,21 @@
|
||||
class Counter {
|
||||
value = 3;
|
||||
scale(factor: number): number { return this.value * factor; }
|
||||
}
|
||||
class DerivedCounter extends Counter {
|
||||
scale(factor: number): number { return super.scale(factor) + 1; }
|
||||
}
|
||||
const counter = new Counter();
|
||||
const derived = new DerivedCounter();
|
||||
const original = Counter.prototype['scale'];
|
||||
Counter.prototype.scale = (factor: number): number => factor + 10;
|
||||
console.log(counter.scale(2), derived.scale(2));
|
||||
Counter.prototype.scale = original;
|
||||
console.log(counter['scale'](2), derived['scale'](2));
|
||||
class Alternate {
|
||||
scale(factor: number): number { return factor * 10; }
|
||||
}
|
||||
const selected = Math.random() < 2 ? counter : new Alternate();
|
||||
Counter.prototype.scale = (factor: number): number => factor + 20;
|
||||
console.log(selected.scale(2));
|
||||
Counter.prototype.scale = original;
|
||||
@@ -43,6 +43,9 @@ console.log(map.get(point));
|
||||
const bytes = new Uint8Array([4, 5]);
|
||||
map.set(bytes, bytes);
|
||||
console.log(map.has(bytes), map.get(bytes) === bytes);
|
||||
const siblingView = bytes.subarray(0);
|
||||
map.set(siblingView, "view");
|
||||
console.log(map.get(bytes) === bytes, map.get(siblingView), siblingView === bytes);
|
||||
const copy = map.get(bytes) as Uint8Array;
|
||||
copy[0] = 9;
|
||||
console.log(bytes[0]);
|
||||
|
||||
@@ -2,9 +2,7 @@ class Vector {
|
||||
x = 0;
|
||||
length(): number { return this.x; }
|
||||
}
|
||||
// Prototype data is represented; compiled method descriptors are not.
|
||||
// Named slots are represented; bare prototype reflection is not.
|
||||
const prototype = Vector.prototype;
|
||||
const method = Vector.prototype.length;
|
||||
Vector.prototype.length = () => 1;
|
||||
Object.getOwnPropertyDescriptors(Vector.prototype);
|
||||
// End of the prototype reflection diagnostic fixture.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Vector, Select, choose } from 'inferred-returns';
|
||||
|
||||
const value = new Vector();
|
||||
console.log(Select.choose(false, value).x, Select.choose(true, value));
|
||||
console.log(choose(false, value).x, choose(true, value));
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export class Vector {
|
||||
constructor() { this.x = 0; }
|
||||
set(x) { this.x = x; return this; }
|
||||
}
|
||||
|
||||
export class Select {
|
||||
/** @param {boolean} flag @param {Vector} target */
|
||||
static choose(flag, target) {
|
||||
if (flag) return null;
|
||||
return target.set(5);
|
||||
}
|
||||
}
|
||||
|
||||
export function choose(flag, target) {
|
||||
if (flag) return null;
|
||||
return target;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "inferred-returns",
|
||||
"type": "module",
|
||||
"main": "index.js"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { BoxGeometry, InstancedMesh, InterpolateDiscrete, Matrix4, MeshBasicMaterial, NumberKeyframeTrack, Vector3 } from 'three';
|
||||
import { mergeGeometries, mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||
|
||||
const first = new BoxGeometry(1, 1, 1);
|
||||
const second = new BoxGeometry(1, 1, 1).translate(2, 0, 0);
|
||||
const merged = mergeGeometries([first, second]);
|
||||
const welded = mergeVertices(merged);
|
||||
console.log(merged.getAttribute('position').count, welded.getAttribute('position').count);
|
||||
console.log(welded.getAttribute('position').array.constructor === Float32Array);
|
||||
console.log(welded.index.array.constructor === Uint16Array, welded.index.count);
|
||||
console.log(welded.getAttribute('position').getX(24));
|
||||
|
||||
const instances = new InstancedMesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial(), 3);
|
||||
const transform = new Matrix4();
|
||||
for (let i = 0; i < 3; i++) instances.setMatrixAt(i, transform.makeTranslation(i * 2, 0, 0));
|
||||
instances.computeBoundingBox();
|
||||
console.log(instances.boundingBox.getSize(new Vector3()).x, instances.instanceMatrix.array[28]);
|
||||
instances.getMatrixAt(2, transform);
|
||||
console.log(transform.elements[12]);
|
||||
|
||||
const track = new NumberKeyframeTrack('.position[x]', [0, 1], [0, 10]);
|
||||
const interpolant = track.createInterpolant();
|
||||
console.log(track.times.constructor === Float32Array, track.values.constructor === Float32Array);
|
||||
console.log(interpolant.evaluate(0.25)[0], interpolant.evaluate(0.75)[0]);
|
||||
track.setInterpolation(InterpolateDiscrete);
|
||||
console.log(track.createInterpolant().evaluate(0.5)[0]);
|
||||
@@ -1,27 +1,13 @@
|
||||
class-prototype-reflection.ts:6:19 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet
|
||||
class-prototype-reflection.ts:6:19 - error SC1090: class prototype reflection and accessor replacement (named prototype data and methods compile) is not supported yet
|
||||
|
||||
5 | // Prototype data is represented; compiled method descriptors are not.
|
||||
5 | // Named slots are represented; bare prototype reflection is not.
|
||||
6 | const prototype = Vector.prototype;
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
7 | const method = Vector.prototype.length;
|
||||
7 | Object.getOwnPropertyDescriptors(Vector.prototype);
|
||||
|
||||
class-prototype-reflection.ts:7:16 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet
|
||||
class-prototype-reflection.ts:7:34 - error SC1090: class prototype reflection and accessor replacement (named prototype data and methods compile) is not supported yet
|
||||
|
||||
6 | const prototype = Vector.prototype;
|
||||
7 | const method = Vector.prototype.length;
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
8 | Vector.prototype.length = () => 1;
|
||||
|
||||
class-prototype-reflection.ts:8:1 - error SC1090: assignment to non-variables are not supported yet
|
||||
|
||||
7 | const method = Vector.prototype.length;
|
||||
8 | Vector.prototype.length = () => 1;
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~
|
||||
9 | Object.getOwnPropertyDescriptors(Vector.prototype);
|
||||
|
||||
class-prototype-reflection.ts:9:34 - error SC1090: class prototype reflection and compiled method replacement (named prototype data properties compile) is not supported yet
|
||||
|
||||
8 | Vector.prototype.length = () => 1;
|
||||
9 | Object.getOwnPropertyDescriptors(Vector.prototype);
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
10 | // End of the prototype reflection diagnostic fixture.
|
||||
7 | Object.getOwnPropertyDescriptors(Vector.prototype);
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
8 | // End of the prototype reflection diagnostic fixture.
|
||||
@@ -146,6 +146,7 @@ for (const backend of ["llvm"] as const) {
|
||||
["escape-barrel.ts", "auto", ["escape-string-regexp"]],
|
||||
["nested/main.ts", "auto", ["shouty"]],
|
||||
["slash-cli.ts", ["slash"], ["slash"]],
|
||||
["inferred-returns.mjs", ["inferred-returns"], ["inferred-returns"]],
|
||||
] as const) {
|
||||
const result = lowerRequest(file, {
|
||||
entry: join(root, "tests/fixtures/npm-static", file),
|
||||
|
||||
@@ -19,7 +19,9 @@ function run(command: string, args: string[]) {
|
||||
return { stdout: result.stdout, stderr, status: result.status, signal: result.signal };
|
||||
}
|
||||
|
||||
test.for(["native", "wasm32-wasi"])("published spatial queries and output arrays run statically on %s", async (target, context) => {
|
||||
const cases = ["three/collision.mjs", "three/buffers-animation.mjs", "npm-static/array-output-parameters.mjs"].flatMap((fixture) =>
|
||||
["native", "wasm32-wasi"].map((target) => ({ fixture, target })));
|
||||
test.for(cases)("published $fixture runs statically on $target", async ({ fixture, target }, context) => {
|
||||
if (target === "wasm32-wasi" && !hasZig) context.skip();
|
||||
const dir = await mkdtemp("/tmp/scriptc-spatial-");
|
||||
const previousTarget = process.env["SCRIPTC_TARGET"], previousCc = process.env["SCRIPTC_CC"];
|
||||
@@ -30,26 +32,24 @@ test.for(["native", "wasm32-wasi"])("published spatial queries and output arrays
|
||||
await writeFile(join(pkg,"package.json"),'{"name":"spatial-fixture","type":"module","main":"index.js"}');
|
||||
const entry = join(dir,"main.mjs");
|
||||
await writeFile(entry,'import "spatial-fixture";');
|
||||
for (const fixture of ["three/collision.mjs", "npm-static/array-output-parameters.mjs"]) {
|
||||
const source = join(import.meta.dirname,"../fixtures",fixture);
|
||||
const bundle = await rollup({ input:source, plugins:[{name:"three-cpu-entry",resolveId(id) {
|
||||
if(id === "three") return require.resolve("three/src/Three.Core.js");
|
||||
if(id.startsWith("three/")) return require.resolve(id);
|
||||
return null;
|
||||
}}] });
|
||||
try { await bundle.write({file:join(pkg,"index.js"),format:"es"}); }
|
||||
finally { await bundle.close(); }
|
||||
const wasm = target === "wasm32-wasi";
|
||||
const output = join(dir,wasm ? "program.wasm" : "program");
|
||||
const result = await compile(entry,{outDir:dir,outPath:output,backend:"llvm",dynamic:false,npmStatic:["spatial-fixture"],
|
||||
sanitize:!wasm && process.env["SCRIPTC_SAN"] === "1"});
|
||||
if(!result.ok) throw new Error(result.diagnostics.map(d=>`${d.code}: ${d.message}`).join("\n"));
|
||||
if(wasm) expect([...(await readFile(output)).subarray(0,4)]).toEqual([0,97,115,109]);
|
||||
const reference = run(process.execPath,["--no-warnings",source]);
|
||||
expect(reference.status).toBe(0);
|
||||
expect(run(process.execPath,["--no-warnings",entry])).toEqual(reference);
|
||||
expect(wasm ? run(process.execPath,["--no-warnings","-e",wasiRunner,output]) : run(output,[])).toEqual(reference);
|
||||
}
|
||||
const source = join(import.meta.dirname,"../fixtures",fixture);
|
||||
const bundle = await rollup({ input:source, plugins:[{name:"three-cpu-entry",resolveId(id) {
|
||||
if(id === "three") return require.resolve("three/src/Three.Core.js");
|
||||
if(id.startsWith("three/")) return require.resolve(id);
|
||||
return null;
|
||||
}}] });
|
||||
try { await bundle.write({file:join(pkg,"index.js"),format:"es"}); }
|
||||
finally { await bundle.close(); }
|
||||
const wasm = target === "wasm32-wasi";
|
||||
const output = join(dir,wasm ? "program.wasm" : "program");
|
||||
const result = await compile(entry,{outDir:dir,outPath:output,backend:"llvm",dynamic:false,npmStatic:["spatial-fixture"],
|
||||
sanitize:!wasm && process.env["SCRIPTC_SAN"] === "1"});
|
||||
if(!result.ok) throw new Error(result.diagnostics.map(d=>`${d.code}: ${d.message}`).join("\n"));
|
||||
if(wasm) expect([...(await readFile(output)).subarray(0,4)]).toEqual([0,97,115,109]);
|
||||
const reference = run(process.execPath,["--no-warnings",source]);
|
||||
expect(reference.status).toBe(0);
|
||||
expect(run(process.execPath,["--no-warnings",entry])).toEqual(reference);
|
||||
expect(wasm ? run(process.execPath,["--no-warnings","-e",wasiRunner,output]) : run(output,[])).toEqual(reference);
|
||||
} finally {
|
||||
if(previousTarget === undefined) delete process.env["SCRIPTC_TARGET"]; else process.env["SCRIPTC_TARGET"] = previousTarget;
|
||||
if(previousCc === undefined) delete process.env["SCRIPTC_CC"]; else process.env["SCRIPTC_CC"] = previousCc;
|
||||
|
||||
Reference in New Issue
Block a user