Expand static Effect support across common APIs (#598)

* Expand static Effect support across common APIs

* Preserve compiler parity across expanded Effect patterns

* Fix compiler regressions exposed by Effect coverage

* Bound shared type conversion traversal

* Clarify conversion contracts and tidy fixtures

* Run every Effect fixture in CI shards

* Preserve inferred array assignments in concurrent Effect programs

* Record inferred array fixture compiler ordering
This commit is contained in:
Chris Tate
2026-10-01 02:49:31 -05:00
committed by GitHub
parent 4d022037c3
commit 2069684b40
281 changed files with 5136 additions and 802 deletions
+3 -1
View File
@@ -58,7 +58,7 @@ jobs:
# san = ASan + RC audit over the same suite; anything but "1" is plain.
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
# Within-file partition for the large case-driven harnesses
# (differential, llvm-differential, npm, server, Test262, coverage, and
# (Effect, differential, llvm-differential, npm, server, Test262, coverage, and
# Vercel E2E): they hash stable case names into slice i of 10
# (tests/harness/shard.ts), so the ten shards' unions run every case
# exactly once. vitest's own --shard is FILE-granular and cannot split
@@ -88,6 +88,7 @@ jobs:
- name: Tests (file-sharded, case-sharded files excluded)
run: >-
pnpm test --shard=${{ matrix.shard }}/10
--exclude=tests/harness/effect.test.ts
--exclude=tests/harness/differential.test.ts
--exclude=tests/harness/llvm-differential.test.ts
--exclude=tests/harness/npm.test.ts
@@ -152,6 +153,7 @@ jobs:
SCRIPTC_LLVM_TEST_MODE: dev
run: >-
pnpm test
tests/harness/effect.test.ts
tests/harness/differential.test.ts
tests/harness/llvm-differential.test.ts
tests/harness/npm.test.ts
@@ -64,6 +64,7 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
case "dyn":
case "classval":
case "promise":
case "caught":
return true;
case "object":
return tracedShapes.has(`object:${t.className}`);
+20 -3
View File
@@ -17,6 +17,7 @@ import {
mangleClassTrace,
mangleCtorThunk,
mangleFunction,
mangleGlobal,
mangleVtInstance,
mangleVtStruct,
} from "../mangle.js";
@@ -537,18 +538,34 @@ export function emitClassObjDefs(
let localOwner = meta;
let localValue = "%class";
let localIndex = 0;
while (localOwner.def.localBaseCapture !== undefined && localOwner.base) {
while (localOwner.base) {
if (localOwner.def.localBaseCapture === undefined && localOwner.def.baseValueGlobal === undefined) {
localOwner = localOwner.base;
continue;
}
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
const id = localIndex++;
const base = localOwner.base;
localBaseInits.push(
if (localOwner.def.baseValueGlobal !== undefined) {
host.declare(`declare ptr @scr_classobj_retain_v(ptr)`);
localBaseInits.push(
` %base.raw${id} = load ptr, ptr @${mangleGlobal(localOwner.def.baseValueGlobal)}`,
` %base.value${id} = call ptr @scr_classobj_retain_v(ptr %base.raw${id})`,
);
} else localBaseInits.push(
` %base.caps${id} = getelementptr inbounds %ScrClassObj, ptr ${localValue}, i64 1`,
` %base.cap${id} = getelementptr inbounds ptr, ptr %base.caps${id}, ${host.sizeType} ${localOwner.def.localBaseCapture}`,
` %base.box${id} = load ptr, ptr %base.cap${id}`,
` %base.value${id} = call ptr @scr_box_get_ref(ptr %base.box${id})`,
);
if (base.def.localCaptures !== undefined) localBaseInits.push(
` %base.slot${id} = getelementptr inbounds %${mangleClassStruct(base.def.name)}, ptr %o, i64 0, i32 ${classEnvironmentIndex(base)}`,
` store ptr %base.value${id}, ptr %base.slot${id}`,
);
else {
host.declare(`declare void @scr_classobj_release_v(ptr)`);
localBaseInits.push(` call void @scr_classobj_release_v(ptr %base.value${id})`);
}
localOwner = base;
localValue = `%base.value${id}`;
}
@@ -567,7 +584,7 @@ export function emitClassObjDefs(
` ret ptr %o`,
`}`,
`@${mangleClassObj(className)} = internal global %ScrClassObj ` +
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0}, ptr null } ; class ${className}`,
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0}, ptr null, ptr null } ; class ${className}`,
``,
);
}
+143 -82
View File
@@ -25,7 +25,7 @@ import { InternalCompilerError } from "../../errors.js";
* ScrDynPath { parent, key, index } — the %ScrDynPath type. */
import type { IrType, IrUnionDef } from "../../ir/ir.js";
import { DYN_HANDLE_KINDS, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { dynDesc, undefinedArmTag } from "../../ir/analysis.js";
import { dynDesc } from "../../ir/analysis.js";
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { llvmCommentText } from "./common.js";
@@ -85,6 +85,7 @@ export class LlDyn {
private readonly dynFuncBoxes = new Map<string, string>();
private readonly dynFuncAdapters = new Map<string, string>();
private readonly promiseDynAdapters = new Map<string, string>();
private readonly mapDynOps = new Map<string, string>();
private dynToStrFn: string | null = null;
private caughtToDynFn: string | null = null;
private readonly helperMemo = new Map<string, string>();
@@ -129,7 +130,38 @@ export class LlDyn {
/** The value LLVM type of a dynCheck result / toDyn operand for `t`. */
private valTy(t: IrType): string {
return t.kind === "f64" ? "double" : t.kind === "bool" ? "i1" : "ptr";
return t.kind === "f64" || t.kind === "date" ? "double" : t.kind === "bool" ? "i1" : "ptr";
}
private collectionDynOps(t: IrType & { kind: "map" | "set" }): string {
const key = typeKey(t);
const prior = this.mapDynOps.get(key);
if (prior) return prior;
const name = `sc_map_dyn_${this.mapDynOps.size}`;
this.mapDynOps.set(key, name);
const keyType = t.kind === "map" ? t.key : t.elem;
const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" };
const emitSlot = (type: IrType, role: string): void => {
const ty = this.valTy(type);
const unpack = ty === "double" ? "bitcast i64 %slot to double" : ty === "i1" ? "trunc i64 %slot to i1" : "inttoptr i64 %slot to ptr";
const pack = ty === "double" ? "bitcast double %value to i64" : ty === "i1" ? "zext i1 %value to i64" : "ptrtoint ptr %value to i64";
const box = this.toDynHelper(type);
const unbox = this.dynCheckHelper(type);
this.defs.push(
`define internal ptr @${name}_${role}_box(i64 %slot) ${FN_ATTRS} {`,
` %value = ${unpack}`,
` %result = call ptr @${box}(${ty} %value)`,
` ret ptr %result`, `}`, "",
`define internal i64 @${name}_${role}_unbox(ptr %d) ${FN_ATTRS} {`,
` %value = call ${ty === "i1" ? "zeroext i1" : ty} @${unbox}(ptr %d, ptr null)`,
` %result = ${pack}`,
` ret i64 %result`, `}`, "",
);
};
emitSlot(keyType, "key");
emitSlot(valueType, "value");
this.defs.push(`@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`, "");
return name;
}
/* ── dyn node plumbing ─────────────────────────────────────────────── */
@@ -247,6 +279,14 @@ export class LlDyn {
return t;
}
/** Ordinary property lookup, including inherited data and accessors (+1). */
private objReadLit(B: BlockBuilder, d: string, key: string): string {
this.host.declare(`declare ptr @scr_dyn_obj_read(ptr, ptr, ${this.S})`);
const value = B.tmp();
B.line(`${value} = call ptr @scr_dyn_obj_read(ptr ${d}, ptr ${this.host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")})`);
return value;
}
/** Appends an ScrStr's bytes into a ScrJsonBuf (walkers' putScrStr). */
private putScrStr(B: BlockBuilder, buf: string, s: string): void {
this.host.declare(`declare void @scr_jb_putc(ptr, i8)`);
@@ -392,6 +432,13 @@ export class LlDyn {
B.terminate(`ret i1 ${r}`);
};
switch (t.kind) {
case "date": {
this.host.declare(`declare zeroext i1 @scr_dyn_native_date_is(ptr)`);
const r = B.tmp();
B.line(`${r} = call zeroext i1 @scr_dyn_native_date_is(ptr %d)`);
B.terminate(`ret i1 ${r}`);
break;
}
case "url": {
this.host.declare(`declare zeroext i1 @scr_dyn_native_url_is(ptr)`);
const r = B.tmp();
@@ -408,15 +455,19 @@ export class LlDyn {
}
case "map":
case "set": {
this.host.declare(`declare zeroext i1 @scr_dyn_native_${t.kind}_is(ptr)`);
this.host.declare(`declare zeroext i1 @scr_dyn_native_collection_is(ptr, i32, ptr)`);
const r = B.tmp();
B.line(`${r} = call zeroext i1 @scr_dyn_native_${t.kind}_is(ptr %d)`);
const typed = t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn";
B.line(`${r} = call zeroext i1 @scr_dyn_native_collection_is(ptr %d, i32 ${t.kind === "map" ? 1 : 0}, ptr ${typed ? this.host.cstr(typeKey(t)) : "null"})`);
B.terminate(`ret i1 ${r}`);
break;
}
case "bigint":
kindIs(DYN_KIND.BIGINT);
break;
case "promise":
kindIs(DYN_KIND.PROMISE);
break;
case "symbol":
kindIs(DYN_KIND.SYMBOL);
break;
@@ -518,26 +569,25 @@ export class LlDyn {
const isObj = B.tmp();
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
const l0 = B.newLabel("dm.o");
B.condBr(isObj, l0, fail);
let readable = isObj;
if (!shape.indexValue) {
const proxy = B.tmp();
readable = B.tmp();
B.line(`${proxy} = icmp eq i32 ${kd}, ${DYN_KIND.PROXY}`);
B.line(`${readable} = or i1 ${isObj}, ${proxy}`);
}
B.condBr(readable, l0, fail);
B.startBlock(l0);
// dyn ('unknown') fields match ANY value, present or missing.
for (const f of shape.fields) {
if (f.type.kind === "dyn") continue;
const m = this.objGetLit(B, "%d", f.name);
const has = B.tmp();
B.line(`${has} = icmp ne ptr ${m}, null`);
const lTest = B.newLabel("dm.t");
const m = this.objReadLit(B, "%d", f.name);
const lNext = B.newLabel("dm.n");
if (undefinedArmTag(f.type, this.host.unionsById) >= 0) {
// Optional-flavored field: a MISSING key is the undefined arm
// (a match); a PRESENT key must fit the union as usual.
B.condBr(has, lTest, lNext);
} else {
B.condBr(has, lTest, fail);
}
B.startBlock(lTest);
this.host.declare(`declare void @scr_dyn_release(ptr)`);
this.pendingBail(B, "dm.read", () => B.line(`call void @scr_dyn_release(ptr ${m})`), "i1 false");
const ok = B.tmp();
B.line(`${ok} = call zeroext i1 @${this.dynMatchHelper(f.type)}(ptr ${m})`);
B.line(`call void @scr_dyn_release(ptr ${m})`);
B.condBr(ok, lNext, fail);
B.startBlock(lNext);
}
@@ -680,6 +730,17 @@ export class LlDyn {
}
if (isRefCounted(t) && t.kind !== "dyn") {
host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`);
if (t.kind === "object" && t.className === "%Error") {
host.declare(`declare ptr @scr_errdyn_err_of(ptr)`);
const error = B.tmp(), found = B.tmp();
B.line(`${error} = call ptr @scr_errdyn_err_of(ptr %d)`);
B.line(`${found} = icmp ne ptr ${error}, null`);
const yes = B.newLabel("dc.error"), next = B.newLabel("dc.noterror");
B.condBr(found, yes, next);
B.startBlock(yes);
B.terminate(`ret ptr ${error}`);
B.startBlock(next);
}
const matched = this.typedRefMatches(B, t);
const lRef = B.newLabel("dc.tr");
const lNext = B.newLabel("dc.nt");
@@ -821,6 +882,13 @@ export class LlDyn {
B.startBlock(lo);
};
switch (t.kind) {
case "date": {
host.declare(`declare double @scr_dyn_native_date_value(ptr)`);
const r = B.tmp();
B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`);
B.terminate(`ret double ${r}`);
break;
}
case "url": {
host.declare(`declare ptr @scr_dyn_native_url_check(ptr, ptr)`);
const r = B.tmp();
@@ -837,9 +905,10 @@ export class LlDyn {
}
case "map":
case "set": {
host.declare(`declare ptr @scr_dyn_native_${t.kind}_check(ptr, ptr)`);
host.declare(`declare ptr @scr_dyn_native_collection_check(ptr, i32, ptr, ptr)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_native_${t.kind}_check(ptr %d, ptr %path)`);
const typed = t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn";
B.line(`${r} = call ptr @scr_dyn_native_collection_check(ptr %d, i32 ${t.kind === "map" ? 1 : 0}, ptr ${typed ? host.cstr(typeKey(t)) : "null"}, ptr %path)`);
B.terminate(`ret ptr ${r}`);
break;
}
@@ -911,6 +980,14 @@ export class LlDyn {
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
B.terminate(`ret ptr null`);
break;
case "promise": {
requireKind(DYN_KIND.PROMISE, "dc");
host.declare(`declare ptr @scr_promise_retain(ptr)`);
const promise = this.payloadOf(B, "%d", "ptr"), retained = B.tmp();
B.line(`${retained} = call ptr @scr_promise_retain(ptr ${promise})`);
B.terminate(`ret ptr ${retained}`);
break;
}
case "object": {
// The %Error extraction (an instanceof-Error narrow on unknown):
// validate the checked-dynamic tree's error encoding — the reserved "%error" marker
@@ -1023,7 +1100,19 @@ export class LlDyn {
B.terminate(`ret ptr %r0`);
break;
}
requireKind(DYN_KIND.OBJ, "dcr");
if (shape.indexValue) requireKind(DYN_KIND.OBJ, "dcr");
else {
const kind = this.kindOf(B, "%d"), object = B.tmp(), proxy = B.tmp(), readable = B.tmp();
B.line(`${object} = icmp eq i32 ${kind}, ${DYN_KIND.OBJ}`);
B.line(`${proxy} = icmp eq i32 ${kind}, ${DYN_KIND.PROXY}`);
B.line(`${readable} = or i1 ${object}, ${proxy}`);
const ok = B.newLabel("dcr.object"), fail = B.newLabel("dcr.fail");
B.condBr(readable, ok, fail);
B.startBlock(fail);
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
B.terminate(`ret ptr null`);
B.startBlock(ok);
}
if (shape.fields.length === 0 && !shape.indexValue) {
const sourceAccessor = `${this.toDynHelper(t)}_source_access`;
const rc = vAdapters(host, t);
@@ -1041,59 +1130,15 @@ export class LlDyn {
}
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
for (const f of shape.fields) {
const fieldWant = host.cstr(dynDesc(f.type, this.host.recordsById, this.host.unionsById));
const utag = f.type.kind === "union" ? undefinedArmTag(f.type, host.unionsById) : -1;
const m = this.objGetLit(B, "%d", f.name);
if (f.type.kind === "dyn") {
// An `unknown` field: a present key passes through, a missing
// one IS the undefined dyn value. Go through the dyn builder so
// a Web-stream typed-ref capsule cannot escape inside a checked
// record field.
const has = B.tmp();
B.line(`${has} = icmp ne ptr ${m}, null`);
const sel = B.tmp();
const u = this.undef(B);
B.line(`${sel} = select i1 ${has}, ptr ${m}, ptr ${u}`);
const value = B.tmp();
B.line(`${value} = call ptr @${this.dynCheckHelper(f.type)}(ptr ${sel}, ptr ${pathSlot})`);
storeInto(f.name, f.type, value);
} else if (utag >= 0 && f.type.kind === "union") {
const unit = host.unitInstanceRef(f.type.unionId, utag);
const has = B.tmp();
B.line(`${has} = icmp ne ptr ${m}, null`);
const lp = B.newLabel("dcr.p");
const la = B.newLabel("dcr.a");
const lj = B.newLabel("dcr.j");
B.condBr(has, lp, la);
B.startBlock(la);
storeInto(f.name, f.type, unit); // absent key -> the undefined arm
B.br(lj);
B.startBlock(lp);
setPath(f.name, "0");
const v = B.tmp();
B.line(`${v} = call ptr @${this.dynCheckHelper(f.type)}(ptr ${m}, ptr ${pathSlot})`);
storeInto(f.name, f.type, v);
this.pendingBail(B, "dcr", releaseR, "ptr null");
B.br(lj);
B.startBlock(lj);
} else {
const has = B.tmp();
B.line(`${has} = icmp ne ptr ${m}, null`);
const lp = B.newLabel("dcr.p");
const la = B.newLabel("dcr.a");
B.condBr(has, lp, la);
B.startBlock(la);
setPath(f.name, "0");
B.line(`call void @scr_dyn_check_fail(ptr ${pathSlot}, ptr ${fieldWant}, ptr null)`);
releaseR();
B.terminate(`ret ptr null`);
B.startBlock(lp);
setPath(f.name, "0");
const v = B.tmp();
B.line(`${v} = call ${this.valTy(f.type)} @${this.dynCheckHelper(f.type)}(ptr ${m}, ptr ${pathSlot})`);
storeInto(f.name, f.type, v);
this.pendingBail(B, "dcr", releaseR, "ptr null");
}
const m = this.objReadLit(B, "%d", f.name);
host.declare(`declare void @scr_dyn_release(ptr)`);
this.pendingBail(B, "dcr.read", () => { B.line(`call void @scr_dyn_release(ptr ${m})`); releaseR(); }, "ptr null");
setPath(f.name, "0");
const value = B.tmp();
B.line(`${value} = call ${this.valTy(f.type)} @${this.dynCheckHelper(f.type)}(ptr ${m}, ptr ${pathSlot})`);
B.line(`call void @scr_dyn_release(ptr ${m})`);
storeInto(f.name, f.type, value);
this.pendingBail(B, "dcr", releaseR, "ptr null");
}
// Index-signature shapes CAPTURE undeclared keys into the
// overflow map.
@@ -1423,6 +1468,10 @@ export class LlDyn {
case "map":
case "set": {
host.declare(`declare ptr @scr_dyn_native_${t.kind}(ptr)`);
if (t.kind === "map" ? t.key.kind !== "dyn" || t.value.kind !== "dyn" : t.elem.kind !== "dyn") {
host.declare(`declare void @scr_map_dyn_attach(ptr, ptr)`);
B.line(`call void @scr_map_dyn_attach(ptr %v, ptr @${this.collectionDynOps(t)})`);
}
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_native_${t.kind}(ptr %v)`);
B.terminate(`ret ptr ${r}`);
@@ -2925,14 +2974,13 @@ export class LlDyn {
const materialized = B.tmp();
const out = B.tmp();
B.line(`${materialized} = call ptr @scr_dyn_typed_ref_materialize(ptr %d)`);
B.line(`${out} = call ptr @${name}(ptr ${materialized}, ${host.sizeType} %n)`);
B.line(`${out} = call ptr @${name}(ptr ${materialized}, ${host.sizeType} %n, ptr %spelling)`);
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
B.terminate(`ret ptr ${out}`);
B.startBlock(lNext);
}
// ISLAND-held sources: an engine array IS iterable — the not-iterable
// TypeError below would be a wrong claim. Loud fence (lane
// dyn-routing-ops).
// Island-held sources keep the engine's iterator and IteratorClose
// behavior while returning checked values for the destructuring slots.
{
const isJv = B.tmp();
B.line(`${isJv} = icmp eq i32 ${kd}, ${DYN_KIND.JSVAL}`);
@@ -2940,10 +2988,12 @@ export class LlDyn {
const lNotJv = B.newLabel("din.njv");
B.condBr(isJv, lJv, lNotJv);
B.startBlock(lJv);
host.declare(`declare zeroext i1 @scr_dyn_isl_fence(ptr, ptr)`);
const f = B.tmp();
B.line(`${f} = call zeroext i1 @scr_dyn_isl_fence(ptr %d, ptr ${host.cstr("iteration")})`);
B.terminate(`ret ptr null`);
host.declare(`declare ptr @scr_dyn_jsval_iter_n(ptr, double)`);
const count = B.tmp();
const out = B.tmp();
B.line(`${count} = uitofp ${host.sizeType} %n to double`);
B.line(`${out} = call ptr @scr_dyn_jsval_iter_n(ptr %d, double ${count})`);
B.terminate(`ret ptr ${out}`);
B.startBlock(lNotJv);
}
const okA = B.tmp();
@@ -2964,6 +3014,17 @@ export class LlDyn {
const buf = "%dib";
B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`);
B.line(`call void @scr_jb_init(ptr ${buf})`);
const hasSpelling = B.tmp();
B.line(`${hasSpelling} = icmp ne ptr %spelling, null`);
const named = B.newLabel("din.named"), unnamed = B.newLabel("din.unnamed");
B.condBr(hasSpelling, named, unnamed);
B.startBlock(named);
B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spelling)`);
const namedMessage = B.tmp();
B.line(`${namedMessage} = call ptr @scr_jb_finish(ptr ${buf})`);
B.line(`call void @scr_throw_error(i32 1, ptr ${namedMessage})`);
B.terminate(`ret ptr null`);
B.startBlock(unnamed);
const lU = B.newLabel("din.u");
const lN = B.newLabel("din.n");
const lB2 = B.newLabel("din.b");
@@ -3146,7 +3207,7 @@ export class LlDyn {
});
B.terminate(`ret ptr ${out}`);
this.defs.push(
`define internal ptr @${name}(ptr %d, ${host.sizeType} %n) ${FN_ATTRS} { ; destructuring GetIterator + N steps`,
`define internal ptr @${name}(ptr %d, ${host.sizeType} %n, ptr %spelling) ${FN_ATTRS} { ; destructuring GetIterator + N steps`,
B.render(),
`}`,
``,
+57 -10
View File
@@ -45,7 +45,7 @@ import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/c
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
import { computeMayThrow } from "../may-throw.js";
import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleClassStruct, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper } from "../mangle.js";
import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleClassStruct, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper, mangleVtInstance } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { LlvmDebugInfo } from "./debug-info.js";
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText } from "./common.js";
@@ -89,6 +89,7 @@ import {
releaseSym,
retainSym,
traceArg,
typedRefConstructor,
vAdapters,
} from "./shapes.js";
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
@@ -815,6 +816,24 @@ export class LlEmitter {
// emit), then the file assembles around them — the runtime ABI’s order.
const fnDefs: string[] = [];
for (const fn of this.mod.functions) fnDefs.push(this.emitFunction(fn));
const errorViews: string[] = [];
for (const property of ["name", "message"]) if (this.mod.functions.some((fn) => fn.name === `%error.${property}.read`)) {
this.declare(`declare void @scr_error_install_${property}_reader(ptr)`);
errorViews.push(` call void @scr_error_install_${property}_reader(ptr @${mangleFunction(`%error.${property}.read`)})`);
}
for (const meta of this.classMeta.values()) {
if (meta.def.runtime || meta.root.def.name !== "%Error") continue;
const type: IrType = { kind: "object", className: meta.def.name };
const adapter = this.liveDynRefAdapter(type);
const rc = vAdapters(this.shapeHost, type);
const key = typeKey(type);
const name = `sc_error_view_${errorViews.length}`;
this.resolveThunkDefs.push(`define internal ptr @${name}(ptr %v) ${FN_ATTRS} {`, `entry:`,
` %d = call ptr ${typedRefConstructor(this.shapeHost, type)}(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ptr ${this.cstr(key)}, ${this.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
` ret ptr %d`, `}`, ``);
this.declare(`declare void @scr_error_register_dyn(ptr, ptr)`);
errorViews.push(` call void @scr_error_register_dyn(ptr @${mangleVtInstance(meta.def.name)}, ptr @${name})`);
}
const layouts = emitLlvmLayouts(this.shapeHost, this.mod, this.classMeta, this.classObjs, this.fnByName, (t) => this.llType(t));
const shapes = layouts.records;
const classShapes = layouts.classes;
@@ -864,8 +883,9 @@ export class LlEmitter {
// sc_release_globals tail). Only when the dispatch unit is even
// linked (defineProps is the only writer).
const runtimeFeatures = moduleRuntimeFeatures(this.mod);
const fnValueProps = runtimeFeatures.dynInvoke ? [...this.fnValues] : [];
const fnValueProps = [...this.fnValues];
if (fnValueProps.length > 0) this.declare(`declare void @scr_box_release(ptr)`);
if (this.classObjs.size > 0) this.declare(`declare void @scr_classobj_clear_properties(ptr)`);
const globalReleaseLines = (prefix: string): string[] => {
const lines: string[] = [];
globals.forEach((g, i) => {
@@ -884,6 +904,7 @@ export class LlEmitter {
` store ptr null, ptr getelementptr inbounds (%ScrClosure, ptr @${mangleFnClosure(name)}, i64 0, i32 3)`,
);
});
for (const name of this.classObjs.keys()) lines.push(` call void @scr_classobj_clear_properties(ptr @${mangleClassObj(name)})`);
return lines;
};
// Exit listeners can read MODULE GLOBALS directly, so they must run
@@ -1124,8 +1145,8 @@ export class LlEmitter {
// The runtime error prefix { rc, vt, name, message, code, cause } and the
// class-object shape { rc, pre, post, ctor, name } — field reads on
// builtin errors and classval loads GEP through these.
`%ScrError = type { ${this.sizeType}, ptr, ptr, ptr, ptr, ptr }`,
`%ScrClassObj = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr }`,
`%ScrError = type { ${this.sizeType}, ptr, ptr, ptr, ptr, ptr, i8, i8, i8, i8, i8, ptr, ptr }`,
`%ScrClassObj = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`,
// The runtime emitter prefix { rc, vt, reg, cls } — user subclasses
// embed it (classes.ts), and bare-emitter GEPs address through it.
`%ScrEmitter = type { ${this.sizeType}, ptr, ptr, ptr }`,
@@ -1325,7 +1346,7 @@ export class LlEmitter {
// scr_lib_init(argc, argv), then the entry function. An uncaught
// exception escaping top-level code prints and exits 1 (Node).
// The exception epilogue is emitted when the entry may throw.
const stamps: string[] = [];
const stamps: string[] = [...errorViews];
for (const iv of this.errorIntervals) {
const fields: [number, number][] = [[0, iv.pre], [1, iv.post]];
for (const [field, value] of fields) {
@@ -2050,6 +2071,10 @@ export class LlEmitter {
this.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
tr.push(` call void @scr_promise_fulfill_str(ptr %pr, ptr %r) ; moves in`);
break;
case "dyn":
this.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`);
tr.push(` call void @scr_promise_resolve_dyn(ptr %pr, ptr %r) ; moves in`);
break;
default: {
const v = vAdapters(this.shapeHost, ret);
this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
@@ -3184,6 +3209,10 @@ export class LlEmitter {
this.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`);
this.B.line(`call void @scr_promise_fulfill_str(ptr ${pr}, ptr ${v!.name}) ; moves in`);
break;
case "dyn":
this.declare(`declare void @scr_promise_resolve_dyn(ptr, ptr)`);
this.B.line(`call void @scr_promise_resolve_dyn(ptr ${pr}, ptr ${v!.name}) ; moves in`);
break;
default: {
const rc = vAdapters(this.shapeHost, ret);
this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
@@ -3577,6 +3606,17 @@ export class LlEmitter {
} else {
this.storeField(ptr, type, v.name);
}
if (s.kind === "fieldSet" && s.field === "name" && this.classMeta.get(s.className)?.root.def.name === "%Error") {
const present = this.classFieldPtr(obj.name, s.className, "%namePresent").ptr;
const enumerable = this.classFieldPtr(obj.name, s.className, "%nameEnumerable").ptr;
const wasPresent = B.tmp(), wasEnumerable = B.tmp(), isPresent = B.tmp(), nextEnumerable = B.tmp();
B.line(`${wasPresent} = load i8, ptr ${present}`);
B.line(`${wasEnumerable} = load i8, ptr ${enumerable}`);
B.line(`${isPresent} = icmp ne i8 ${wasPresent}, 0`);
B.line(`${nextEnumerable} = select i1 ${isPresent}, i8 ${wasEnumerable}, i8 1`);
B.line(`store i8 1, ptr ${present}`);
B.line(`store i8 ${nextEnumerable}, ptr ${enumerable}`);
}
break;
}
case "recordKeyDelete": {
@@ -4073,10 +4113,9 @@ export class LlEmitter {
case "rethrow": {
// Re-raise the saved snapshot (payload retained — the binding
// local releases with its scope) and unwind like `throw`.
const c = B.tmp();
B.line(`${c} = load ptr, ptr %${mangleLocal(s.localId)}`);
const c = this.emitExpr({ kind: "varRef", localId: s.localId, type: CAUGHT, loc: s.loc });
this.declare(`declare void @scr_rethrow(ptr)`);
B.line(`call void @scr_rethrow(ptr ${c})`);
B.line(`call void @scr_rethrow(ptr ${c.name})`);
this.emitUnwind();
break;
}
@@ -4199,8 +4238,16 @@ export class LlEmitter {
this.emitBlock(s.catchBody!, (scope) => {
const c = B.tmp();
B.line(`${c} = call ptr @scr_exc_take() ; catch binding`);
B.line(`store ptr ${c}, ptr ${slot}`);
scope.push({ slot, type: CAUGHT });
if (this.currentLocals.get(s.catchLocalId!)?.boxed) {
const box = B.tmp();
B.line(`${box} = ${boxNewCall(this.shapeHost, CAUGHT)} ; captured exception`);
this.boxSet(box, CAUGHT, c);
B.line(`store ptr ${box}, ptr ${slot}`);
scope.push({ slot, type: CAUGHT, boxed: true });
} else {
B.line(`store ptr ${c}, ptr ${slot}`);
scope.push({ slot, type: CAUGHT });
}
});
} else {
this.declare(`declare void @scr_exc_clear()`);
@@ -46,7 +46,7 @@ export function emitControlExpr(host: LlvmEmitterContext, e: ExprOf<"dynDestrChe
}
const helper = host.dyn.dynIterNHelper();
const t = B.tmp();
B.line(`${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count})`);
B.line(`${t} = call ptr @${helper}(ptr ${v.name}, ${host.sizeType} ${e.count}, ptr ${e.notIterableMessage ? host.cstr(e.notIterableMessage) : "null"})`);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
return out;
@@ -39,7 +39,8 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
(host.unionsById.get(v.type.unionId)?.arms.some(isDynTypedRefType) ?? false));
// 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) {
const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className);
if (!errorRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
if (v.type.kind === "union") {
const adapter = host.liveDynUnionRefAdapter(v.type);
const boxed = B.tmp();
@@ -498,9 +499,8 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
B.line(`${test} = call zeroext i1 @scr_dyn_truthy(ptr ${d.name})`);
} else if (e.test === "error") {
// `u instanceof Error`: the checked-dynamic tree's error encoding — an object
// carrying the reserved "%error" marker key — or a real engine
// Error held by reference (the isl helper answers false for
// every non-JSVAL kind, so the call is unconditional).
// carrying the reserved "%error" marker key, a registered native
// Error view, or a real engine Error held by reference.
const kd = host.dynKind(d.name);
const isObj = B.tmp();
B.line(`${isObj} = icmp eq i32 ${kd}, ${DYN_KIND.OBJ}`);
@@ -519,7 +519,9 @@ export function emitDynamicExpr(host: LlvmEmitterContext, e: ExprOf<"dynFrom" |
const has = B.tmp();
B.line(`${m} = call ptr @scr_dyn_obj_get(ptr ${d.name}, ptr ${host.cstr("%error")}, ${host.sizeType} 6)`);
B.line(`${has} = icmp ne ptr ${m}, null`);
B.line(`store i1 ${has}, ptr ${slot}`);
const branded = B.tmp();
B.line(`${branded} = or i1 ${has}, ${isl}`);
B.line(`store i1 ${branded}, ptr ${slot}`);
B.br(lj);
B.startBlock(lj);
test = B.tmp();
@@ -2,8 +2,8 @@ import { typedRefConstructor } from "./shapes.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { streamTypedRefEligible } from "../../ir/analysis.js";
import { type IrType, DYN_CLASS_PROPERTIES, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { mangleFunction, mangleRecordStruct } from "../mangle.js";
import { type IrType, DYN_CLASS_PROPERTIES, SYMBOL_T, classDynViewSupported, isClassOwnEnumerableFieldName, isDynTypedRefType, isRefCounted, typeKey } from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { classFieldIndex, classStructSym } from "./classes.js";
import { llvmCommentText } from "./common.js";
@@ -148,7 +148,9 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
`define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit unknown class ${typeKey(t)}`,
`entry:`,
];
const fields = meta.def.fields.filter((field) => isClassOwnEnumerableFieldName(field.name));
const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]));
const fields = meta.def.fields.filter((field) => (isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) &&
!(meta.root.def.name === "%Error" && (field.name === "message" || field.name === "name")));
for (const [index, field] of fields.entries()) {
const next = `f${index}_next`;
const raw = `f${index}_raw`;
@@ -161,12 +163,43 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
const { index: fieldIndex } = classFieldIndex(meta, field.name);
const fieldTy = llFieldType(field.type);
const checkTy = field.type.kind === "f64" ? "double" : field.type.kind === "bool" ? "i1" : "ptr";
const symbolGlobal = symbols.get(field.name);
if (symbolGlobal) {
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
lines.push(
` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`,
` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`,
);
if (field.type.kind === "symbol") {
// A base constructor can mutate the instance before this
// derived field initializes. An absent snapshot entry leaves
// that native slot uninitialized until its declaration runs.
host.declare(`declare zeroext i1 @scr_dyn_has_own_computed(ptr, ptr)`);
lines.push(
` %f${index}_present = call zeroext i1 @scr_dyn_has_own_computed(ptr %d, ptr %f${index}_key)`,
` %f${index}_priorptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${fieldIndex}`,
` %f${index}_prior = load ptr, ptr %f${index}_priorptr`,
` %f${index}_initialized = icmp ne ptr %f${index}_prior, null`,
` %f${index}_read = or i1 %f${index}_present, %f${index}_initialized`,
` br i1 %f${index}_read, label %f${index}_read_symbol, label %f${index}_absent_symbol`,
`f${index}_absent_symbol:`,
` call void @scr_dyn_release_v(ptr %f${index}_key)`,
` br label %${after}`,
`f${index}_read_symbol:`,
);
}
lines.push(
` %${raw} = call ptr @scr_dyn_symbol_key_get(ptr %d, ptr %f${index}_key, i1 zeroext false)`,
` call void @scr_dyn_release_v(ptr %f${index}_key)`,
);
} else lines.push(` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`);
lines.push(
` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
` %${missing} = icmp eq ptr %${raw}, null`,
` %${undef} = call ptr @scr_dyn_undefined()`,
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
` %${pending} = call zeroext i1 @scr_exc_pending()`,
` br i1 %${pending}, label %done, label %${store}`,
`${store}:`,
@@ -201,20 +234,30 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
`bag_fail:`, ` call void @scr_dyn_release_v(ptr %bag)`, ` br label %done`, `bag_keys:`,
);
if (meta.def.prototypeDataHelper) {
if (meta.def.instancePrototypeHelper || meta.def.prototypeDataHelper) {
host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`);
lines.push(
` %prototype = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`,
...(meta.def.instancePrototypeHelper ? [
` %prototype_receiver = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %target)`,
` %prototype = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr %prototype_receiver)`,
] : [` %prototype = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`]),
` %with_prototype = call ptr @scr_dyn_set_prototype(ptr %bag, ptr %prototype)`,
` call void @scr_dyn_release_v(ptr %prototype)`,
` call void @scr_dyn_release_v(ptr %with_prototype)`,
);
}
fields.forEach((field, index) => lines.push(
fields.filter((field) => !symbols.has(field.name)).forEach((field, index) => lines.push(
` %bag_key${index} = call ptr @scr_str_new(ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
` call void @scr_dyn_key_delete(ptr %bag, ptr %bag_key${index}, i1 zeroext false)`,
` call void @scr_str_release(ptr %bag_key${index})`,
));
if (symbols.size > 0) host.declare(`declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`);
[...symbols.values()].forEach((globalId, index) => lines.push(
` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`,
` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`,
` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
));
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
lines.push(
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
@@ -223,6 +266,10 @@ export function streamTypedRefCommitAdapter(host: LlvmEmitterContext,
` call void @scr_dyn_release_v(ptr %bag_old)`,
);
}
if (meta.root.def.name === "%Error") {
host.declare(`declare void @scr_error_commit_dyn(ptr, ptr)`);
lines.push(` call void @scr_error_commit_dyn(ptr %target, ptr %d)`);
}
lines.push(` br label %done`, `done:`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...lines);
return `@${commit}`;
@@ -469,17 +516,25 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
host.declare(`declare ptr @scr_dyn_new_obj()`);
host.declare(`declare void @scr_dyn_obj_set(ptr, ptr, ${host.sizeType}, ptr)`);
const out = B.tmp();
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
if (meta.def.prototypeDataHelper) {
if (meta.root.def.name === "%Error") {
host.declare(`declare ptr @scr_error_dyn_fields(ptr)`);
B.line(`${out} = call ptr @scr_error_dyn_fields(ptr %p)`);
} else B.line(`${out} = call ptr @scr_dyn_new_obj()`);
if (meta.def.instancePrototypeHelper || meta.def.prototypeDataHelper) {
host.declare(`declare ptr @scr_dyn_set_prototype(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
const prototype = B.tmp(), attached = B.tmp();
B.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper)}()`);
if (meta.def.instancePrototypeHelper) {
const receiver = B.tmp();
B.line(`${receiver} = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %p)`);
B.line(`${prototype} = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr ${receiver})`);
} else B.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`);
B.line(`${attached} = call ptr @scr_dyn_set_prototype(ptr ${out}, ptr ${prototype})`);
B.line(`call void @scr_dyn_release_v(ptr ${prototype})`);
B.line(`call void @scr_dyn_release_v(ptr ${attached})`);
}
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name))) {
for (const field of meta.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name) &&
!(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")))) {
const { index } = classFieldIndex(meta, field.name);
const fieldPtr = B.tmp();
let fieldValue = B.tmp();
@@ -493,6 +548,38 @@ export function streamTypedRefMaterializeAdapter(host: LlvmEmitterContext,
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
B.line(`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`);
}
for (const symbol of meta.def.symbolFields ?? []) {
const field = meta.def.fields.find((field) => field.name === symbol.field)!;
const { index } = classFieldIndex(meta, symbol.field);
const fieldPtr = B.tmp(), rawKey = B.tmp();
let fieldValue = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`);
B.line(`${fieldValue} = load ${llFieldType(field.type)}, ptr ${fieldPtr}`);
const afterSymbol = field.type.kind === "symbol" ? B.newLabel("symbol.after") : null;
if (afterSymbol) {
const initialized = B.tmp(), present = B.newLabel("symbol.present");
B.line(`${initialized} = icmp ne ptr ${fieldValue}, null`);
B.condBr(initialized, present, afterSymbol);
B.startBlock(present);
}
if (llFieldType(field.type) === "i8") {
const boolValue = B.tmp();
B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`);
fieldValue = boolValue;
}
B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`);
const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey);
const value = host.streamTypedRefBoxValue(B, field.type, fieldValue);
host.declare(`declare void @scr_dyn_symbol_key_set(ptr, ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
B.line(`call void @scr_dyn_symbol_key_set(ptr ${out}, ptr ${key}, ptr ${value})`);
B.line(`call void @scr_dyn_release_v(ptr ${key})`);
B.line(`call void @scr_dyn_release_v(ptr ${value})`);
if (afterSymbol) {
B.terminate(`br label %${afterSymbol}`);
B.startBlock(afterSymbol);
}
}
if (meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)) {
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
@@ -537,6 +537,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"bigint.bufferWrite": "scr_bigint_buffer_write",
"bigint.dataViewGet": "scr_bigint_dataview_get",
"bigint.dataViewSet": "scr_bigint_dataview_set",
"dyn.sameValue": "scr_dyn_same_value",
"error.toString": "scr_error_to_string",
"error.stack": "scr_error_stack",
"error.stackLimitGet": "scr_stack_limit_get",
@@ -576,6 +577,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.globalSymbolHas": "scr_dyn_global_symbol_has",
"dyn.globalSymbolDelete": "scr_dyn_global_symbol_delete",
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
"dyn.classIs": "scr_dyn_class_is_key",
"dyn.iterPack": "scr_dyn_iter_pack",
"dyn.iterator": "scr_dyn_iterator",
"dyn.arrayFromIterator": "scr_dyn_array_from_iterator",
@@ -603,6 +605,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
"dyn.toString": "scr_dyn_to_string_argument",
"dyn.this": "scr_dyn_this_get",
"dyn.generatorThis": "scr_gen_receiver",
"insp.dyn": "scr_insp_dyn",
"insp.dynS": "scr_insp_dyn_s",
"fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn",
@@ -914,8 +917,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.assignAll": "scr_dyn_assign_all",
"dyn.objCreateNullProto": "scr_dyn_new_obj_null_proto",
"dyn.arrayPrototype": "scr_dyn_array_prototype",
"dyn.objectPrototype": "scr_dyn_object_prototype",
"dyn.functionApply": "scr_dyn_function_apply",
"dyn.builtinMethod": "scr_dyn_builtin_method",
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.objCreate": "scr_dyn_obj_create",
@@ -923,6 +930,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.getPrototype": "scr_dyn_get_prototype",
"dyn.setPrototype": "scr_dyn_set_prototype",
"dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names",
"dyn.ownKeys": "scr_dyn_own_keys",
"dyn.getOwnPropertySymbols": "scr_dyn_get_own_property_symbols",
"dyn.getOwnPropertyDescriptors": "scr_dyn_get_own_property_descriptors",
"dyn.preventExtensions": "scr_dyn_prevent_extensions",
@@ -106,6 +106,9 @@ export function releaseSym(host: ShapeHost, t: IrType): string {
* cannot participate in a cycle. */
export function traceAdapter(host: ShapeHost, t: IrType): string | null {
switch (t.kind) {
case "caught":
host.declare(`declare void @scr_caught_trace_v(ptr, ptr, ptr)`);
return "@scr_caught_trace_v";
case "dyn":
host.declare(`declare void @scr_dyn_trace_v(ptr, ptr, ptr)`);
return "@scr_dyn_trace_v";
@@ -0,0 +1,122 @@
import { BOOL, DYN, STRING, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js";
import { dynUndefinedExpr, type Lowerer } from "./lowerer.js";
import type { ParamShape } from "./lower-calls.js";
import { classPrototypeData } from "./class-prototypes.js";
const PREFIX = "%class.construct:";
/** Checked construction dispatches to emitted native constructor thunks. */
export function checkedClassConstruction(lowerer: Lowerer, callee: IrExpr, args: IrExpr[], loc: SrcLoc): IrExpr {
const name = `${PREFIX}${args.length}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const params = [callee, ...args].map((_, index) => ({ localId: `p.${index}`, name: `p${index}`, type: DYN }));
lowerer.liftedFns.push({ name, params, locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
returnType: DYN, body: [{ kind: "return", value: { kind: "libCall", fn: "bytes.construct", args: [
varRef("p.0", DYN, loc), { kind: "dynArrLit", elems: params.slice(1).map((param) => varRef(param.localId, DYN, loc)), type: DYN, loc },
{ kind: "strLit", value: "constructor", type: STRING, loc },
], type: DYN, loc }, loc }], loc });
}
return { kind: "call", callee: name, args: [callee, ...args], type: DYN, loc };
}
function constructorArguments(lowerer: Lowerer, params: ParamShape[], incoming: IrExpr[], loc: SrcLoc): IrExpr[] | null {
const args: IrExpr[] = [];
let index = 0;
for (const param of params) {
if ((param.mode === "dynRest" || param.mode === "arguments") && param.type.kind === "dyn") {
args.push({ kind: "dynArrLit", elems: param.mode === "arguments" ? incoming : incoming.slice(index), type: DYN, loc });
index = incoming.length;
} else if (param.mode === "rest" && param.type.kind === "array") {
const elems = incoming.slice(index).map((value) => lowerer.coerceToExpected(value, param.type.kind === "array" ? param.type.elem : DYN));
if (elems.some((value) => !typeEquals(value.type, param.type.kind === "array" ? param.type.elem : DYN))) return null;
args.push({ kind: "arrayLit", elems, type: param.type, loc });
index = incoming.length;
} else {
if (param.mode !== "required" && param.mode !== "omittable") return null;
const value = incoming[index++] ?? param.callDefault ?? dynUndefinedExpr(loc);
const converted = lowerer.coerceToExpected(value, param.type);
if (!typeEquals(converted.type, param.type)) return null;
args.push(converted);
}
}
return args;
}
export class ClassConstructionDispatch {
private readonly constructors = new Set<string>();
private readonly completed = new Map<string, Set<string>>();
private prototypeHelper: IrFunction | null = null;
private readonly prototypes = new Set<string>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
const discover = (type: IrType): void => {
if (type.kind === "classval") this.constructors.add(type.className);
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
};
const prototypeReads = new Set<IrFunction>();
for (const fn of functions) everyStmtList(fn.body, { stmt: () => true, expr: (expr) => {
if (expr.kind === "dynFrom") discover(expr.value.type);
if (expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype") prototypeReads.add(fn);
return true;
} });
let changed = false;
for (const fn of functions) {
if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue;
fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => {
if (expr.kind !== "libCall" || expr.fn !== "dyn.classBasePrototype") return expr;
if (!this.prototypeHelper) {
const loc = expr.loc;
this.prototypeHelper = { name: "%class.basePrototype", params: [{ localId: "value", name: "value", type: DYN }],
locals: [{ id: "value", name: "value", type: DYN, mutable: false }], returnType: DYN, loc,
body: [{ kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }] };
lowerer.liftedFns.push(this.prototypeHelper);
changed = true;
}
return { kind: "call", callee: this.prototypeHelper.name, args: expr.args, type: DYN, loc: expr.loc };
} });
}
if (this.prototypeHelper) for (const name of this.constructors) {
if (this.prototypes.has(name)) continue;
this.prototypes.add(name);
const info = lowerer.classes.get(name);
if (!info) continue;
const loc = this.prototypeHelper.loc;
const type: IrType = { kind: "classval", className: name };
const value = varRef("value", DYN, loc);
const prototype = classPrototypeData(lowerer, info, loc, { kind: "dynCheck", value, type, loc });
if (!prototype) continue;
const resolved = transformExpr(prototype, { stmt: (stmt) => stmt, expr: (expr) =>
expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype"
? { kind: "call", callee: this.prototypeHelper!.name, args: expr.args, type: DYN, loc: expr.loc } : expr });
this.prototypeHelper.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.classIs", args: [value,
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
then: [{ kind: "return", value: resolved, loc }], else_: null, loc });
changed = true;
}
for (const fn of functions) {
if (!fn.name.startsWith(PREFIX)) continue;
let completed = this.completed.get(fn.name);
if (!completed) this.completed.set(fn.name, completed = new Set());
const loc = fn.loc;
for (const name of this.constructors) {
if (completed.has(name)) continue;
completed.add(name);
const info = lowerer.classes.get(name);
if (!info || info.generic || info.def.runtime) continue;
const args = constructorArguments(lowerer, info.ctorParams, fn.params.slice(1).map((param) => varRef(param.localId, DYN, loc)), loc);
if (!args) continue;
const type: IrType = { kind: "classval", className: name };
const callee = varRef("p.0", DYN, loc);
const value: IrExpr = { kind: "newValue", callee: { kind: "dynCheck", value: callee, type, loc }, args, type: { kind: "object", className: name }, loc };
const body: IrStmt[] = [{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc }];
fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.classIs", args: [callee,
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: body, else_: null, loc });
lowerer.noteEdge(`%${name}.constructor`);
changed = true;
}
}
return changed;
}
}
@@ -62,8 +62,9 @@ test("discovers dispatch sites added during a later reachability pass", () => {
test("known class property bags preserve one owned receiver without boxing", () => {
const { lowerer, liftedFns } = context();
lowerer.isSubclassOf = () => false;
const type = { kind: "object", className: "Widget" } as const;
const info = { def: { name: "Widget", fields: [] }, fields: new Map(), base: null, subclasses: [] } as unknown as ClassInfo;
const info = { def: { name: "Widget", fields: [] }, fields: new Map(), methods: new Map(), base: null, subclasses: [] } as unknown as ClassInfo;
lowerer.classes.set("Widget", info);
const receiver: IrExpr = { kind: "call", callee: "makeWidget", args: [], type, loc };
const target = fn("target", [statement({ kind: "call", callee: "%dyn.class.properties",
@@ -5,13 +5,14 @@ import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traver
import { dynUndefinedExpr, PoisonError, type Lowerer } from "./lowerer.js";
import { implicitDefaultInstance, type ParamShape } from "./lower-calls.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 { classPrototypeData, hasClassPrototypeData, reflectClassPrototype } from "./class-prototypes.js";
import { errorPropertyRead, errorPropertyWrite, errorToStringCall, errorToStringMethod, refreshErrorPropertyDispatch, refreshErrorMethodDispatch } from "./error-methods.js";
import { GeneratorDynamicDispatch } from "./generator-dynamic-dispatch.js";
import { classMethodValue } from "./class-method-values.js";
import * as ts from "../ts7/adapter.js";
import { SYMBOL_T } from "../../ir/ir.js";
import { isClassCallback } from "./class-callbacks.js";
import { ClassConstructionDispatch } from "./class-construction.js";
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
interface Dispatch {
@@ -34,14 +35,31 @@ interface PropertyDispatch {
classes: Set<string>;
}
export function classInstanceOf(lowerer: Lowerer, value: IrExpr, info: ClassInfo, loc: SrcLoc): IrExpr {
const name = `%dyn.class.instanceof:${info.def.name}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
name, params: [{ localId: "value", name: "value", type: DYN }], returnType: BOOL,
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
body: [{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc }], loc,
});
return { kind: "call", callee: name, args: [value], type: BOOL, loc };
export function classInstanceOf(lowerer: Lowerer, value: IrExpr, info: ClassInfo, loc: SrcLoc, classValue?: IrExpr): IrExpr {
const name = `%dyn.class.${classValue ? "valueInstanceof" : "instanceof"}:${info.def.name}`;
const params = [{ localId: "value", name: "value", type: DYN },
...(classValue ? [{ localId: "class", name: "class", type: classValue.type }] : [])];
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const candidate = varRef("candidate", DYN, loc);
const prototype = classPrototypeData(lowerer, info, loc, classValue ? varRef("class", classValue.type, loc) : undefined);
const body: IrStmt[] = prototype ? [
{ kind: "varDecl", localId: "candidate", init: varRef("value", DYN, loc), loc },
{ kind: "while", cond: { kind: "ternary", cond: { kind: "dynTest", test: "nullish", value: candidate, type: BOOL, loc },
then: { kind: "boolLit", value: false, type: BOOL, loc }, else_: { kind: "dynTest", test: "object", value: candidate, type: BOOL, loc }, type: BOOL, loc }, body: [
{ kind: "assign", localId: "candidate", value: { kind: "libCall", fn: "dyn.getPrototype", args: [candidate], type: DYN, loc }, loc },
{ kind: "if", cond: { kind: "dynScalarEq", left: candidate, right: prototype, type: BOOL, loc }, then: [
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
], else_: null, loc },
], loc },
] : [];
body.push({ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc });
lowerer.liftedFns.push({
name, params, returnType: BOOL,
locals: [...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...(prototype ? [{ id: "candidate", name: "candidate", type: DYN, mutable: true }] : [])], body, loc,
});
}
return { kind: "call", callee: name, args: [value, ...(classValue ? [classValue] : [])], type: BOOL, loc };
}
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc): IrFunction {
@@ -65,6 +83,7 @@ export class ClassDynamicDispatch {
constructor(private readonly asyncFree = false) {}
private readonly generatorDispatch = new GeneratorDynamicDispatch();
private readonly constructionDispatch = new ClassConstructionDispatch();
private readonly boxed = new Set<string>();
private readonly dispatches = new Map<string, Dispatch>();
private readonly properties = new Map<string, PropertyDispatch>();
@@ -79,10 +98,17 @@ export class ClassDynamicDispatch {
private readonly constructors = new Map<string, ClassInfo>();
private readonly constructed = new Set<string>();
private readonly instanceTests = new Map<string, Set<string>>();
private readonly jsonPrototypes = new Set<string>();
private descriptorHelper: IrFunction | null = null;
private prototypeHelper: IrFunction | null = null;
private readonly prototypeClasses = new Set<string>();
private readonly instancePrototypes = new Map<string, { fn: IrFunction; classes: Set<string> }>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
let changed = refreshErrorMethodDispatch(lowerer, functions);
changed = refreshErrorPropertyDispatch(lowerer, functions) || changed;
changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed;
changed = this.constructionDispatch.process(lowerer, functions) || changed;
const seenTypes = new Set<string>();
const rewrite = new Set<IrFunction>();
const discover = (type: IrType): void => {
@@ -115,7 +141,7 @@ export class ClassDynamicDispatch {
break;
case "libCall":
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);
expr.fn === "dyn.hasKeyComputed" || expr.fn === "dyn.hasOwnComputed" || expr.fn === "dyn.propertyIsEnumerableComputed" || expr.fn === "dyn.defineProperty" || expr.fn === "dyn.getPrototype") rewrite.add(fn);
break;
}
return true;
@@ -123,8 +149,8 @@ export class ClassDynamicDispatch {
});
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
for (const fn of functions) {
if (!fn.name.startsWith("%dyn.class.instanceof:")) continue;
const target = fn.name.slice("%dyn.class.instanceof:".length);
if (!fn.name.startsWith("%dyn.class.instanceof:") && !fn.name.startsWith("%dyn.class.valueInstanceof:")) continue;
const target = fn.name.slice(fn.name.indexOf(":") + 1);
let checked = this.instanceTests.get(fn.name);
if (!checked) { checked = new Set(); this.instanceTests.set(fn.name, checked); }
for (const className of this.boxed) {
@@ -135,7 +161,9 @@ export class ClassDynamicDispatch {
const loc = fn.loc;
const value = varRef("value", DYN, loc);
const type: IrType = { kind: "object", className };
const result: IrExpr = subtype ? { kind: "boolLit", value: true, type: BOOL, loc }
const result: IrExpr = subtype && fn.params[1] && lowerer.classes.get(target)?.localClass
? { kind: "instanceOfValue", value: { kind: "dynCheck", value, type, loc }, classValue: varRef("class", fn.params[1].type, loc), type: BOOL, loc }
: subtype ? { kind: "boolLit", value: true, type: BOOL, loc }
: { kind: "instanceOf", value: { kind: "dynCheck", value, type, loc }, className: target, type: BOOL, loc };
fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value,
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc }, then: [{ kind: "return", value: result, loc }], else_: null, loc });
@@ -150,6 +178,75 @@ export class ClassDynamicDispatch {
for (const className of this.boxed) {
const info = lowerer.classes.get(className);
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
if (info.symbolFields && info.def.symbolFields === undefined) {
info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => {
const global = lowerer.globalsBySymbol.get(symbol);
return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : [];
});
}
if (this.prototypeHelper && !this.prototypeClasses.has(className)) {
this.prototypeClasses.add(className);
const loc = this.prototypeHelper.loc;
const type: IrType = { kind: "object", className };
const value = varRef("value", DYN, loc);
const prototype = reflectClassPrototype(lowerer, info, loc, { kind: "dynCheck", value, type, loc });
if (prototype) this.prototypeHelper.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value,
{ kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
then: [{ kind: "return", value: prototype, loc }], else_: null, loc });
changed = true;
}
if (!this.jsonPrototypes.has(className) && info.decl && findMethodOn(lowerer, info, "toJSON")) {
this.jsonPrototypes.add(className);
const loc = this.propertyBag.loc;
const method = classMethodValue(lowerer, info.decl, info, "toJSON", loc);
const prototype = method ? classPrototypeData(lowerer, info, loc) : null;
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
const init = helper?.body[0];
if (!info.prototypeReflectionReady && method && prototype && init?.kind === "if" && canConvertToDyn(method.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
const descriptor: IrStmt = { kind: "exprStmt", expr: {
kind: "libCall", fn: "dyn.defineProperty", args: [prototype,
{ kind: "dynFrom", value: { kind: "strLit", value: "toJSON", type: STRING, loc }, type: DYN, loc },
{ kind: "dynObjLit", fields: [
{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: { kind: "dynFrom", value: method, type: DYN, loc } },
...["writable", "configurable"].map((key): { key: IrExpr; value: IrExpr } => ({ key: { kind: "strLit", value: key, type: STRING, loc }, value: { kind: "dynFrom", value: { kind: "boolLit", value: true, type: BOOL, loc }, type: DYN, loc } })),
], type: DYN, loc },
], type: DYN, loc }, loc };
init.then.push(descriptor);
changed = true;
}
}
}
for (const plan of this.propertyReceivers(lowerer)) {
const info = plan.info;
const className = info.def.name;
let instancePrototype = this.instancePrototypes.get(plan.capsule.def.name);
if (!instancePrototype && hasClassPrototypeData(info)) {
const name = `%class.instancePrototype:${plan.capsule.def.name}`;
const type: IrType = { kind: "object", className: plan.capsule.def.name };
const loc = this.propertyBag.loc;
const fn: IrFunction = { name, params: [{ localId: "this", name: "this", type }],
locals: [{ id: "this", name: "this", type, mutable: false }], returnType: DYN,
body: [{ kind: "return", value: { kind: "libCall", fn: "dyn.getPrototype",
args: [{ kind: "dynObjLit", fields: [], type: DYN, loc }], type: DYN, loc }, loc }], loc };
plan.capsule.def.instancePrototypeHelper = name;
instancePrototype = { fn, classes: new Set() };
this.instancePrototypes.set(plan.capsule.def.name, instancePrototype);
this.generated.add(fn);
lowerer.liftedFns.push(fn);
}
if (instancePrototype && hasClassPrototypeData(info) && !instancePrototype.classes.has(className)) {
instancePrototype.classes.add(className);
const loc = instancePrototype.fn.loc;
const receiver = varRef("this", instancePrototype.fn.params[0]!.type, loc);
const prototype = classPrototypeData(lowerer, info, loc, info === plan.capsule ? receiver
: { kind: "downcast", value: receiver, type: { kind: "object", className }, loc });
const returned: IrStmt = { kind: "return", value: prototype ?? { kind: "libCall", fn: "dyn.getPrototype",
args: [{ kind: "dynObjLit", fields: [], type: DYN, loc }], type: DYN, loc }, loc };
if (info === plan.capsule) instancePrototype.fn.body[instancePrototype.fn.body.length - 1] = returned;
else instancePrototype.fn.body.unshift({ kind: "if",
cond: { kind: "instanceOf", value: receiver, className, type: BOOL, loc }, then: [returned], else_: null, loc });
changed = true;
}
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
@@ -172,13 +269,11 @@ export class ClassDynamicDispatch {
}
}
}
if (this.bagClasses.has(className)) continue;
this.bagClasses.add(className);
if (this.bagClasses.has(plan.key)) continue;
this.bagClasses.add(plan.key);
this.ensurePropertyBag(info);
const loc = this.propertyBag.loc;
const type: IrType = { kind: "object", className };
const value = varRef("p.0", DYN, loc);
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
const { receiver, condition } = this.propertyReceiver(plan, loc);
const bag: IrExpr = { kind: "fieldGet", obj: receiver, className, field: PROPERTY_BAG, type: DYN, loc };
const prototype = needsPrototype(info) ? prototypeFor(info, receiver, loc) : null;
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = {
@@ -186,9 +281,11 @@ export class ClassDynamicDispatch {
? { kind: "libCall", fn: "dyn.objCreate", args: [prototype], type: DYN, loc }
: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc,
};
this.bagInitializers.set(className, [initialize]);
const initializers = this.bagInitializers.get(className) ?? [];
initializers.push(initialize);
this.bagInitializers.set(className, initializers);
this.propertyBag.body.unshift({
kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs", args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], type: BOOL, loc },
kind: "if", cond: condition,
then: [
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc }, then: [
initialize,
@@ -218,6 +315,18 @@ export class ClassDynamicDispatch {
fn.body = transformStmtList(fn.body, {
stmt: (stmt) => stmt,
expr: (expr) => {
if (expr.kind === "libCall" && expr.fn === "dyn.getPrototype") {
if (!this.prototypeHelper) {
const loc = expr.loc;
this.prototypeHelper = { name: "%dyn.class.getPrototype", params: [{ localId: "value", name: "value", type: DYN }],
locals: [{ id: "value", name: "value", type: DYN, mutable: false }], returnType: DYN, loc,
body: [{ kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }] };
lowerer.liftedFns.push(this.prototypeHelper);
this.generated.add(this.prototypeHelper);
changed = true;
}
return { kind: "call", callee: this.prototypeHelper.name, args: expr.args, type: DYN, loc: expr.loc };
}
if (expr.kind === "libCall" && (expr.fn === "dyn.toString" || expr.fn === "dyn.stringConstructor") && lowerer.classes.has("%Error")) {
const key = JSON.stringify([expr.fn, ...expr.args.map((arg) => typeKey(arg.type))]);
let errorString = this.errorStrings.get(key);
@@ -266,6 +375,30 @@ export class ClassDynamicDispatch {
}
return { kind: "call", callee: this.constructDispatch.name, args: expr.args, type: DYN, loc: expr.loc };
}
if (expr.kind === "libCall" && expr.fn === "dyn.defineProperty") {
const loc = expr.loc;
if (!this.descriptorHelper) {
const params = ["target", "key", "descriptor"].map((name) => ({ localId: name, name, type: DYN }));
const target = varRef("target", DYN, loc);
const key = varRef("key", DYN, loc);
const descriptor = varRef("descriptor", DYN, loc);
this.descriptorHelper = {
name: "%dyn.class.defineProperty", params, returnType: DYN,
locals: params.map((param) => ({ id: param.localId, name: param.name, type: DYN, mutable: false })),
body: [
{ kind: "if", cond: { kind: "dynTest", test: "symbol", value: key, type: BOOL, loc }, then: [
{ kind: "exprStmt", expr: { ...expr, args: [{ kind: "call", callee: this.propertyBag!.name, args: [target], type: DYN, loc }, key, descriptor] }, loc },
{ kind: "return", value: target, loc },
], else_: null, loc },
{ kind: "return", value: { ...expr, args: [target, key, descriptor] }, loc },
], loc,
};
this.generated.add(this.descriptorHelper);
lowerer.liftedFns.push(this.descriptorHelper);
changed = true;
}
return { kind: "call", callee: this.descriptorHelper.name, args: expr.args, type: DYN, 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;
@@ -474,6 +607,11 @@ export class ClassDynamicDispatch {
const loc = dispatch.fn.loc;
const { receiver, condition } = this.propertyReceiver(plan, loc);
const memberBody = (name: string): IrStmt[] => {
if (dispatch.probe && (name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error") && dispatch.probe !== "in") return [{
kind: "return", value: dispatch.probe === "own"
? { kind: "fieldGet", obj: lowerer.upcastTo(receiver, "%Error"), className: "%Error", field: `%${name}Present`, type: BOOL, loc }
: { kind: "fieldGet", obj: lowerer.upcastTo(receiver, "%Error"), className: "%Error", field: `%${name}Enumerable`, type: BOOL, loc }, loc,
}];
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) {
@@ -507,6 +645,16 @@ export class ClassDynamicDispatch {
}
}
const branches: IrStmt[] = [];
if (!dispatch.write) {
const value = varRef("p.0", DYN, loc);
const key = varRef("p.key", DYN, loc);
const bag: IrExpr = { kind: "call", callee: this.propertyBag!.name, args: [value], type: DYN, loc };
branches.push({ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc }, then: [
{ kind: "return", value: dispatch.probe ? {
kind: "libCall", fn: dispatch.probe === "enumerable" ? "dyn.propertyIsEnumerableComputed" : "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc,
} : { kind: "dynKeyGet", value: bag, key, type: DYN, loc }, loc },
], else_: null, loc });
}
for (const [name, symbol] of symbols) {
const before = lowerer.diags.length;
let then: IrStmt[];
@@ -604,6 +752,9 @@ export class ClassDynamicDispatch {
* receiver as an owned parameter so calls and temporary instances retain
* their ordinary evaluation and lifetime rules without a boxed capsule. */
private typedPropertyBag(lowerer: Lowerer, type: Extract<IrType, { kind: "object" }>, loc: SrcLoc): IrFunction | null {
// A superclass constructor can be the first code to access this bag.
// Its receiver still needs the actual subclass's prototype.
if (lowerer.classes.get(type.className)?.subclasses.length) return null;
const existing = this.typedPropertyBags.get(type.className);
if (existing) return existing;
const initializers = this.bagInitializers.get(type.className);
@@ -680,6 +831,9 @@ export class ClassDynamicDispatch {
private propertyBody(lowerer: Lowerer, dispatch: PropertyDispatch, info: ClassInfo, receiver: IrExpr): IrStmt[] {
const { name, write } = dispatch;
const loc = dispatch.fn.loc;
if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error")) return write
? [errorPropertyWrite(lowerer, receiver, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING), name), { kind: "return", value: null, loc }]
: [{ kind: "return", value: lowerer.coerceToExpected(errorPropertyRead(lowerer, receiver, name), DYN), loc }];
const field = info.fields.get(name);
const member = `${write ? "set" : "get"}:${name}`;
const accessor = field ? null : findMethodOn(lowerer, info, member);
@@ -13,7 +13,9 @@ export function lowerInstanceConstructorNew(lowerer: Lowerer, expr: ts.NewExpres
let access = expr.expression;
while (ts.isParenthesizedExpression(access)) access = access.expression;
if (!ts.isPropertyAccessExpression(access) || access.name.text !== "constructor" || access.questionDotToken) return null;
const receiver = lowerer.lowerExpr(access.expression);
let receiver = lowerer.lowerExpr(access.expression);
const declared = receiver.type.kind === "dyn" ? lowerer.mapTypeOf(lowerer.typeOf(access.expression)) : null;
if (declared?.kind === "object") receiver = lowerer.coerceToExpected(receiver, declared);
if (receiver.type.kind !== "object") return null;
const info = lowerer.classes.get(receiver.type.className);
if (!info || info.def.runtime) return null;
@@ -2,9 +2,9 @@ import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, STRING, RUNTIME_ERROR_CLASSES, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType, type SrcLoc } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { findGenericMethodOn, findMethodOn, genericOverrideBelow, type ClassInfo } from "./lower-classes.js";
import { isJsSourceFile, locOf } from "../program.js";
import { newFnCtx, type Lowerer } from "./lowerer.js";
import { classMemberNameOf, findGenericMethodOn, findMethodOn, genericOverrideBelow, type ClassInfo } from "./lower-classes.js";
import { funcTypeFromParamShapes, implicitDefaultInstance, type ParamShape } from "./lower-calls.js";
import { errorToStringMethod } from "./error-methods.js";
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
@@ -16,7 +16,7 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
if (method === "toString" && findMethodOn(lowerer, info, method)?.declarer.builtinError) {
return errorToStringMethod(lowerer, lowerer.lowerExpr(expr.expression));
}
const value = methodValue(lowerer, expr, info);
let value = methodValue(lowerer, expr, info);
if (!value) return null;
const loc = locOf(expr);
const callback = isClassCallback(lowerer, info, method);
@@ -30,6 +30,11 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
};
const overrides = [...lowerer.classes.values()].filter((candidate) =>
candidate !== info && lowerer.isSubclassOf(candidate.def.name, info.def.name) && candidate.methods.has(method));
const checked = isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type) && overrides.every((candidate) => {
const selected = methodValue(lowerer, expr, candidate);
return selected !== null && lowerer.dynConvertible(selected.type);
});
if (checked && overrides.length) value = lowerer.coerceToExpected(value, DYN);
if (overrides.length === 0) {
return finish({ kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: reference, loc }], result: value, type: value.type, loc });
}
@@ -42,7 +47,8 @@ export function lowerClassMethodValue(lowerer: Lowerer, expr: ts.PropertyAccessE
const body: IrStmt[] = [];
overrides.sort((a, b) => lowerer.isSubclassOf(a.def.name, b.def.name) ? -1 : lowerer.isSubclassOf(b.def.name, a.def.name) ? 1 : 0);
for (const candidate of overrides) {
const selected = methodValue(lowerer, expr, candidate);
let selected = methodValue(lowerer, expr, candidate);
if (selected && checked) selected = lowerer.coerceToExpected(selected, DYN);
if (!selected || !typeEquals(selected.type, value.type)) {
lowerer.unsupported("SC1090", expr, "method values with incompatible override signatures");
}
@@ -63,6 +69,11 @@ export function classMethodValue(lowerer: Lowerer, blame: ts.Node, info: ClassIn
const found = findMethodOn(lowerer, info, method);
const generic = found ? null : findGenericMethodOn(lowerer, info, method);
if (!found && !generic) return null;
const declaration = found?.declarer ?? generic!.declarer;
const reflected = declaration.prototypeMethodValues?.get(method);
if (reflected) return reflected;
const checkedMethod = reflectedClassMethodValue(lowerer, declaration, method);
if (checkedMethod) return checkedMethod;
let owner: ClassInfo;
let params: ParamShape[];
let ret: IrType;
@@ -114,6 +125,45 @@ export function classMethodValue(lowerer: Lowerer, blame: ts.Node, info: ClassIn
return { kind: "closure", fnName: name, captures: [], type, loc };
}
/** Public JS prototype methods are ordinary callables over their actual
* receiver, including descriptor-created copies of class instances. */
export function reflectedClassMethodValue(lowerer: Lowerer, info: ClassInfo, method: string): IrExpr | null {
const existing = info.prototypeMethodValues?.get(method);
if (existing) return existing;
const declaration = info.decl;
if (!declaration) return null;
const member = declaration.members.find((member) => {
if ((!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.name ||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) return false;
const name = classMemberNameOf(lowerer, member.name);
return (ts.isMethodDeclaration(member) ? name : ts.isGetAccessor(member) ? `get:${name}` : ts.isSetAccessor(member) ? `set:${name}` : null) === method;
});
if (!member || (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body) return null;
let needsNativeReceiver = !isJsSourceFile(declaration.getSourceFile()) || info.localClass !== undefined ||
lowerer.errorHierarchyClassOf(info.def.name);
ts.walkPreorder(member, (node) => {
if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword) needsNativeReceiver = true;
});
if (needsNativeReceiver) return null;
const context = newFnCtx(false, null, null, DYN);
const previousClass = lowerer.currentClass;
lowerer.currentClass = info;
lowerer.fnStack.push(context);
try {
const value = lowerer.lowerLambda(member);
if (value.kind === "closure" && value.captures.length === 0) {
const body = lowerer.liftedFns.find((fn) => fn.name === value.fnName);
if (body?.captures?.length === 0) delete body.captures;
}
info.prototypeMethodValues ??= new Map();
info.prototypeMethodValues.set(method, value);
return value;
} finally {
lowerer.fnStack.pop();
lowerer.currentClass = previousClass;
}
}
/** Complete adapters after all native class bodies and instantiations exist. */
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
const byName = new Map(functions.map((fn) => [fn.name, fn]));
@@ -1,11 +1,11 @@
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, STRING, SYMBOL_T, type IrExpr, type IrFunction, type SrcLoc } from "../../ir/ir.js";
import { BOOL, DYN, STRING, SYMBOL_T, type IrExpr, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { locOf } from "../program.js";
import { exactClassOfReceiver, findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
import { classMethodValue } from "./class-method-values.js";
import { classMemberNameOf, classValueRef, exactClassOfReceiver, storedClassValueType, findGenericMethodOn, findMethodOn, type ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";
import { dynUndefinedExpr } from "./lowerer.js";
import { dynUndefinedExpr, newFnCtx } from "./lowerer.js";
import { classMethodValue } from "./class-method-values.js";
/** Data added to a top-level class prototype has shared identity and remains
* separate from instance fields. Allocate lazily so inheritance and module
@@ -18,8 +18,12 @@ export function classPrototypeData(lowerer: Lowerer, info: ClassInfo, loc: SrcLo
kind: "fieldGet", obj: receiver, className: info.def.name, field: `%classEnvironment:${info.def.name}`,
type: { kind: "classval", className: info.def.name }, loc,
};
const base = info.callableBase ? varRef(info.callableBase.prototypeId, DYN, loc)
: info.base && !info.base.builtinError ? classPrototypeData(lowerer, info.base, loc) : null;
const base: IrExpr | null = info.callableBase ? { kind: "libCall", fn: "dyn.classBasePrototype", args: [
{ kind: "libCall", fn: "dyn.getPrototype", args: [{ kind: "dynFrom", value, type: DYN, loc }], type: DYN, loc },
], type: DYN, loc }
: info.base && !info.base.builtinError ? classPrototypeData(lowerer, info.base, loc,
{ kind: "dynCheck", value: { kind: "libCall", fn: "dyn.getPrototype", args: [{ kind: "dynFrom", value, type: DYN, loc }], type: DYN, loc },
type: { kind: "classval", className: info.base.def.name }, loc }) : null;
return { kind: "libCall", fn: "dyn.classPrototype", args: [
{ kind: "dynFrom", value, type: DYN, loc }, base ?? dynUndefinedExpr(loc),
], type: DYN, loc };
@@ -27,7 +31,8 @@ export function classPrototypeData(lowerer: Lowerer, info: ClassInfo, loc: SrcLo
if (info.mixinInstance || info.generic || info.genericInstance || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null;
if (info.def.prototypeDataHelper === undefined) {
const base = info.callableBase ? varRef(info.callableBase.prototypeId, DYN, loc)
: info.base && !info.base.builtinError ? classPrototypeData(lowerer, info.base, loc) : null;
: info.base && !info.base.builtinError ? classPrototypeData(lowerer, info.base, loc,
info.def.baseValueGlobal ? varRef(info.def.baseValueGlobal, { kind: "classval", className: info.base.def.name }, loc) : undefined) : null;
if (info.base && !info.base.builtinError && base === null) return null;
const name = `%prototype.data.${info.def.name}`;
const globalId = `%g.${name}`;
@@ -73,6 +78,72 @@ export function hasClassPrototypeData(info: ClassInfo): boolean {
return info.callableBase !== undefined || info.localPrototypeData === true || info.def.prototypeDataHelper !== undefined || (info.base !== null && hasClassPrototypeData(info.base));
}
/** Populate the shared prototype view before exposing it to reflection.
* Methods and accessors are non-enumerable and retain declaration identity. */
export function reflectClassPrototype(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc, receiver: IrExpr): IrExpr | null {
const prototype = classPrototypeData(lowerer, info, loc, receiver);
if (!prototype || !info.decl) return prototype;
const refuse = (message: string): IrExpr => {
const name = `%prototype.refusal:${info.def.name}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
name, params: [], locals: [], returnType: DYN, body: [{ kind: "runtimeFence", code: "SC1090", message, loc }], loc,
});
info.prototypeReflectionRefusal = name;
return { kind: "call", callee: name, args: [], type: DYN, loc };
};
if (info.prototypeReflectionRefusal) return { kind: "call", callee: info.prototypeReflectionRefusal, args: [], type: DYN, loc };
if (info.localClass) return refuse("prototype reflection on a function-local class is not supported yet");
if (info.base && !info.base.builtinError) {
reflectClassPrototype(lowerer, info.base, loc, info.def.baseValueGlobal
? varRef(info.def.baseValueGlobal, { kind: "classval", className: info.base.def.name }, loc) : receiver);
if (info.base.prototypeReflectionRefusal) return refuse("prototype reflection with this base class is not supported yet");
}
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
const initialization = helper?.body[0];
if (!helper || initialization?.kind !== "if" || info.prototypeReflectionReady) return prototype;
info.prototypeReflectionReady = true;
// Earlier method or JSON views may have installed only observed slots.
// Rebuild them in declaration order when the entire prototype is exposed.
// Lowering passes may have rebuilt these statements, so match their
// operation instead of relying on the original statement's identity.
initialization.then = initialization.then.filter((statement) =>
!(statement.kind === "exprStmt" &&
(statement.expr.kind === "libCall" && statement.expr.fn === "dyn.defineProperty" ||
statement.expr.kind === "call" && statement.expr.callee === "%dyn.class.defineProperty")));
const define = (key: IrExpr, fields: { key: string; value: IrExpr }[]): IrStmt => ({ kind: "exprStmt", expr: {
kind: "libCall", fn: "dyn.defineProperty", args: [prototype, lowerer.coerceToExpected(key, DYN), {
kind: "dynObjLit", fields: [...fields, { key: "configurable", value: { kind: "boolLit" as const, value: true, type: BOOL, loc } }].map((field) => ({
key: { kind: "strLit", value: field.key, type: STRING, loc }, value: lowerer.coerceToExpected(field.value, DYN),
})), type: DYN, loc,
}], type: DYN, loc,
}, loc });
const writable = { key: "writable", value: { kind: "boolLit", value: true, type: BOOL, loc } as IrExpr };
initialization.then.push(define({ kind: "strLit", value: "constructor", type: STRING, loc }, [
{ key: "value", value: classValueRef(lowerer, info, info.decl) }, writable,
]));
const context = newFnCtx(false, null, null, DYN);
lowerer.fnStack.push(context);
try {
for (const member of info.decl.members) {
if ((!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body || ts.isPrivateIdentifier(member.name) ||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) continue;
const name = classMemberNameOf(lowerer, member.name);
if (name === null) return refuse("prototype reflection with an unresolved computed member is not supported yet");
const method = ts.isMethodDeclaration(member) ? name : `${ts.isGetAccessor(member) ? "get" : "set"}:${name}`;
const value = classMethodValue(lowerer, info.decl, info, method, loc);
if (!value || !lowerer.dynConvertible(value.type)) return refuse("prototype reflection with a member that cannot cross a checked value boundary is not supported yet");
const key: IrExpr = ts.isComputedPropertyName(member.name) ? lowerer.lowerExpr(member.name.expression)
: { kind: "strLit", value: member.name.text, type: STRING, loc };
initialization.then.push(define(key, ts.isMethodDeclaration(member) ? [{ key: "value", value }, writable]
: [{ key: ts.isGetAccessor(member) ? "get" : "set", value }]));
}
} finally {
helper.locals.push(...context.locals);
lowerer.fnStack.pop();
}
return prototype;
}
export function isCompiledPrototypeMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
return name === "constructor" || name === "__proto__" ||
!!findMethodOn(lowerer, info, name) || !!findGenericMethodOn(lowerer, info, name) ||
@@ -83,17 +154,23 @@ export function isCompiledPrototypeMember(lowerer: Lowerer, info: ClassInfo, nam
* 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);
const stored = storedClassValueType(lowerer, expr.expression);
const info = exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
const parent = expr.parent;
const descriptorKey = ts.isCallExpression(parent) && parent.arguments[0] === expr &&
ts.isPropertyAccessExpression(parent.expression) && lowerer.stdlibGlobalMember(parent.expression, "Object") === "defineProperty" &&
parent.arguments[1] && ts.isStringLiteral(parent.arguments[1]) ? parent.arguments[1].text : null;
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;
? parent.argumentExpression.text : descriptorKey;
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);
const value = classPrototypeData(lowerer, info, locOf(expr), info.localClass
? lowerer.lowerExprExpecting(expr.expression, { kind: "classval", className: info.def.name }) : undefined);
if (value === null) lowerer.unsupported("SC1090", expr, "prototype data on local, generic, mixin, or runtime-provided classes");
return value;
}
@@ -109,7 +186,8 @@ export function lowerClassPrototypeAssign(lowerer: Lowerer, call: ts.CallExpress
lowerer.unsupported("SC1090", call, "class prototype assignment results and spread arguments");
}
const loc = locOf(call);
const value = classPrototypeData(lowerer, info, loc, info.localClass ? lowerer.lowerExpr(target.expression) : undefined);
const value = classPrototypeData(lowerer, info, loc, info.localClass
? lowerer.lowerExprExpecting(target.expression, { kind: "classval", className: info.def.name }) : undefined);
if (!value) lowerer.unsupported("SC1090", target, "prototype data on generic, mixin, or runtime-provided classes");
const keys: IrExpr[] = ["constructor", "__proto__"].map((name) => ({ kind: "strLit", value: name, type: STRING, loc }));
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
@@ -0,0 +1,63 @@
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, STRING, type IrExpr, type IrStmt } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { locOf } from "../program.js";
import { dynUndefinedExpr, type Lowerer } from "./lowerer.js";
import type { ClassInfo } from "./lower-classes.js";
import { classPrototypeData } from "./class-prototypes.js";
export function hasRuntimeStatics(info: ClassInfo): boolean {
for (let current: ClassInfo | null = info; current; current = current.base) {
if (current.runtimeStatics !== undefined) return true;
}
return false;
}
/** Factory evaluations own their static fields and closures. Defining them
* on the constructor preserves inherited getter receivers and independent
* state when one factory is called repeatedly. */
export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, value: IrExpr): IrExpr {
if (!info.runtimeStatics && !info.runtimePrototypeMembers) return value;
const loc = value.loc;
const local = lowerer.declareHiddenLocal("%staticClass", value.type);
const receiver = varRef(local.id, value.type, loc);
const boxed = lowerer.coerceToExpected(receiver, DYN);
const statements: IrStmt[] = [
{ kind: "varDecl", localId: local.id, init: value, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, dynUndefinedExpr(loc)], type: DYN, loc }, loc },
];
const previousThis = lowerer.ctx.thisLocal;
lowerer.ctx.thisLocal = local;
try {
for (const member of [...(info.runtimeStatics ?? []), ...(info.runtimePrototypeMembers ?? [])]) {
if (ts.isClassStaticBlockDeclaration(member)) {
statements.push(...lowerer.lowerStmts(member.body.statements));
continue;
}
if (!member.name) continue;
const memberLoc = locOf(member);
const key: IrExpr = ts.isComputedPropertyName(member.name)
? lowerer.lowerExprExpecting(member.name.expression, DYN)
: lowerer.coerceToExpected({ kind: "strLit", value: member.name.text!, type: STRING, loc: memberLoc }, DYN);
const fields: { key: string; value: IrExpr }[] = [
{ key: "configurable", value: lowerer.coerceToExpected({ kind: "boolLit", value: true, type: BOOL, loc: memberLoc }, DYN) },
{ key: "enumerable", value: lowerer.coerceToExpected({ kind: "boolLit", value: ts.isPropertyDeclaration(member), type: BOOL, loc: memberLoc }, DYN) },
];
if (ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) {
fields.push({ key: ts.isGetAccessorDeclaration(member) ? "get" : "set", value: lowerer.coerceToExpected(lowerer.lowerLambda(member), DYN) });
} else if (ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member)) {
fields.push({ key: "writable", value: lowerer.coerceToExpected({ kind: "boolLit", value: true, type: BOOL, loc: memberLoc }, DYN) });
fields.push({ key: "value", value: ts.isMethodDeclaration(member)
? lowerer.coerceToExpected(lowerer.lowerLambda(member), DYN)
: member.initializer ? lowerer.lowerExprExpecting(member.initializer, DYN) : dynUndefinedExpr(memberLoc) });
}
const target = info.runtimePrototypeMembers?.includes(member) ? classPrototypeData(lowerer, info, memberLoc, receiver)! : boxed;
statements.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.defineProperty", args: [target, key,
{ kind: "dynObjLit", fields: fields.map((field) => ({ key: { kind: "strLit", value: field.key, type: STRING, loc: memberLoc }, value: field.value })), type: DYN, loc: memberLoc },
], type: DYN, loc: memberLoc }, loc: memberLoc });
}
} finally {
lowerer.ctx.thisLocal = previousThis;
}
return { kind: "seqExpr", stmts: statements, result: receiver, type: receiver.type, loc };
}
@@ -28,8 +28,8 @@ export function classStaticDataFor(lowerer: Lowerer, info: ClassInfo, name: stri
function classStaticData(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrExpr | null {
if (info.localClass || info.mixinInstance || info.generic || info.genericInstance || info.classDecorators || info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError) return null;
if (info.staticDataHelper === undefined) {
const base = info.base ? classStaticData(lowerer, info.base, loc) : null;
if (info.base && base === null) return null;
const base = info.base && !info.base.builtinError ? classStaticData(lowerer, info.base, loc) : null;
if (info.base && !info.base.builtinError && base === null) return null;
const name = `%static.data.${info.def.name}`;
const globalId = `%g.${name}`;
const readyId = `${globalId}.ready`;
@@ -46,6 +46,12 @@ export function lowerStringSplitCall(
type: { kind: "union", unionId: lowerer.unions.intern([{ kind: "nullT" }, UNDEFINED_T]) }, loc,
};
}
if (separator.type.kind === "dyn") {
separator = {
kind: "dynCheck", value: separator,
type: { kind: "union", unionId: lowerer.unions.intern([STRING, F64, BOOL, { kind: "bigint" }, { kind: "nullT" }, UNDEFINED_T]) }, loc,
};
}
const scalar = separator.type.kind === "string" || separator.type.kind === "f64" ||
separator.type.kind === "bool" || separator.type.kind === "bigint" ||
(separator.type.kind === "union" && (lowerer.unions.get(separator.type.unionId)?.arms.every((arm) =>
@@ -1,12 +1,109 @@
import { BOOL, DYN, STRING, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { BOOL, DYN, STRING, VOID, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
import { nodeThrowExpr } from "./lowerer.js";
import { classPrototypeData, hasClassPrototypeData } from "./class-prototypes.js";
import { classMethodValue } from "./class-method-values.js";
import { accessorCall, findMethodOn } from "./lower-classes.js";
const helperName = "%error.toString.method";
const errorType: IrType = { kind: "object", className: "%Error" };
export function errorPropertyRead(lowerer: Lowerer, receiver: IrExpr, property: "name" | "message" = "message"): IrExpr {
const name = `%error.${property}.read`, loc = receiver.loc;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
name, params: [{ localId: "error.0", name: "error", type: errorType }], returnType: STRING,
locals: [{ id: "error.0", name: "error", type: errorType, mutable: false }], body: [], loc,
});
return { kind: "call", callee: name, args: [lowerer.upcastTo(receiver, "%Error")], type: STRING, loc };
}
export function errorPropertyWrite(lowerer: Lowerer, receiver: IrExpr, value: IrExpr, property: "name" | "message" = "message"): IrStmt {
const name = `%error.${property}.write`, loc = receiver.loc;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
name, params: [{ localId: "error.0", name: "error", type: errorType }, { localId: "value", name: "value", type: STRING }], returnType: VOID,
locals: [{ id: "error.0", name: "error", type: errorType, mutable: false }, { id: "value", name: "value", type: STRING, mutable: false }], body: [], loc,
});
return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [lowerer.upcastTo(receiver, "%Error"), value], type: VOID, loc }, loc };
}
export function refreshErrorPropertyDispatch(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
const overrides = [...lowerer.classes.values()].filter((info) => !info.builtinError &&
(hasClassPrototypeData(info) || ["get:name", "set:name", "get:message", "set:message"].some((name) => info.methods.has(name))) &&
lowerer.isSubclassOf(info.def.name, "%Error"));
const properties: ("name" | "message")[] = ["name", "message"];
for (const property of properties) {
if (overrides.length && !lowerer.liftedFns.some((fn) => fn.name === `%error.${property}.read`)) {
errorPropertyRead(lowerer, varRef("error.0", errorType, overrides[0]!.def.loc), property);
return true;
}
}
const readers = functions.filter((fn) => /^%error\.(name|message)\.(read|write)$/.test(fn.name));
if (!readers.length) return false;
overrides.sort((a, b) => lowerer.isSubclassOf(a.def.name, b.def.name) ? -1 : lowerer.isSubclassOf(b.def.name, a.def.name) ? 1 : 0);
const revision = JSON.stringify([readers.map((fn) => fn.name), overrides.map((info) => [info.def.name, hasClassPrototypeData(info)])]);
if (lowerer.errorPropertyDispatchRevision === revision) return false;
lowerer.errorPropertyDispatchRevision = revision;
for (const helper of readers) {
const loc = helper.loc, receiver = varRef("error.0", errorType, loc);
const property = helper.name.includes(".name.") ? "name" : "message";
const key: IrExpr = { kind: "strLit", value: property, type: STRING, loc };
const write = helper.name.endsWith("write");
const own: IrExpr = { kind: "fieldGet", obj: receiver, className: "%Error", field: `%${property}Present`, type: BOOL, loc };
const fallback: IrStmt[] = write ? [
{ kind: "if", cond: { kind: "unary", op: "!", operand: own, type: BOOL, loc }, then: [
{ kind: "fieldSet", obj: receiver, className: "%Error", field: `%${property}Enumerable`, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
], else_: null, loc },
{ kind: "fieldSet", obj: receiver, className: "%Error", field: property, value: varRef("value", STRING, loc), loc },
{ kind: "fieldSet", obj: receiver, className: "%Error", field: `%${property}Present`, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
{ kind: "return", value: null, loc },
] : [{ kind: "return", value: { kind: "fieldGet", obj: receiver, className: "%Error", field: property, type: STRING, loc }, loc }];
const body: IrStmt[] = [{ kind: "if", cond: own, then: fallback, else_: null, loc }];
helper.locals = helper.locals.filter((local) => !local.id.startsWith("descriptor."));
for (const info of overrides) {
const member = `${write ? "set" : "get"}:${property}`;
const value: IrExpr = { kind: "downcast", value: receiver, type: { kind: "object", className: info.def.name }, loc };
const branch: IrStmt[] = [];
const prototype = hasClassPrototypeData(info) ? classPrototypeData(lowerer, info, loc, value) : null;
if (prototype) {
const id = `descriptor.${helper.locals.length}`;
helper.locals.push({ id, name: "descriptor", type: DYN, mutable: false });
const descriptor = varRef(id, DYN, loc);
const read = (name: string): IrExpr => ({ kind: "dynKeyGet", value: descriptor, key: { kind: "strLit", value: name, type: STRING, loc }, type: DYN, loc });
const callback = read(write ? "set" : "get");
const invoke: IrExpr = { kind: "dynCall", callee: callback, receiver: lowerer.coerceToExpected(value, DYN),
calleeName: `Error.${property}`, args: write ? [lowerer.coerceToExpected(varRef("value", STRING, loc), DYN)] : [], type: DYN, loc };
const data: IrStmt[] = write ? [
{ kind: "if", cond: { kind: "dynTest", test: "truthy", negated: true, value: read("writable"), type: BOOL, loc }, then: [
{ kind: "exprStmt", expr: nodeThrowExpr(1, "", `Cannot assign to read only property '${property}'`, VOID, loc), loc },
], else_: null, loc }, ...fallback,
] : [{ kind: "return", value: lowerer.coerceToExpected(read("value"), STRING), loc }];
branch.push({ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [prototype, key], type: BOOL, loc }, then: [
{ kind: "varDecl", localId: id, init: { kind: "libCall", fn: "dyn.getOwnPropertyDescriptor", args: [prototype, lowerer.coerceToExpected(key, DYN)], type: DYN, loc }, loc },
{ kind: "if", cond: { kind: "libCall", fn: "dyn.hasOwn", args: [descriptor, { kind: "strLit", value: "value", type: STRING, loc }], type: BOOL, loc }, then: data, else_: null, loc },
{ kind: "if", cond: { kind: "dynTest", test: "undefined", value: callback, type: BOOL, loc }, then: [
{ kind: "runtimeFence", code: "SC2020", message: `${write ? "writing a getter-only" : "reading a setter-only"} Error.${property} property is not supported`, loc },
], else_: null, loc },
...(write ? [{ kind: "exprStmt", expr: invoke, loc }, { kind: "return", value: null, loc }] as IrStmt[]
: [{ kind: "return", value: lowerer.coerceToExpected(invoke, STRING), loc }] as IrStmt[]),
], else_: null, loc });
}
if (info.methods.has(`get:${property}`) || info.methods.has(`set:${property}`)) {
const accessor = findMethodOn(lowerer, info, member);
branch.push(...(accessor ? write ? [
{ kind: "exprStmt", expr: accessorCall(lowerer, info.def.name, member, value, [varRef("value", STRING, loc)], VOID, loc), loc },
{ kind: "return", value: null, loc },
] : [{ kind: "return", value: lowerer.coerceToExpected(accessorCall(lowerer, info.def.name, member, value, [], accessor.sig.ret, loc), STRING), loc }]
: write ? [{ kind: "exprStmt", expr: nodeThrowExpr(1, "", `Cannot set property ${property} which has only a getter`, VOID, loc), loc }]
: [{ kind: "runtimeFence", code: "SC2020", message: `reading a setter-only Error.${property} property is not supported`, loc }]) as IrStmt[]);
}
if (branch.length) body.push({ kind: "if", cond: { kind: "instanceOf", value: receiver, className: info.def.name, type: BOOL, loc }, then: branch, else_: null, loc });
}
helper.body = [...body, ...fallback];
}
return true;
}
/** Select at extraction time, preserving detached function identity and
* binding the receiver only when the selected function is called. */
export function errorToStringMethod(lowerer: Lowerer, receiver: IrExpr): Extract<IrExpr, { kind: "call" }> {
@@ -671,6 +671,13 @@ export function lowerObjectLiteral(lowerer: Lowerer, expr: ts.ObjectLiteralExpre
}
let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
// An inferred binding pattern supplies placeholders such as
// { nested: { value: any } }. Construct the literal's own fields so
// those placeholders do not erase instantiated generic storage.
if (!expected && ts.isVariableDeclaration(expr.parent) && !expr.parent.type &&
ts.isObjectBindingPattern(expr.parent.name) && expr.parent.initializer === expr) {
tsType = lowerer.typeOf(expr);
}
// `as never` supplies no construction layout. Keep the literal's own
// fields so an exhaustiveness witness cannot erase a reachable value.
if (lowerer.checker.isNeverType(tsType)) tsType = lowerer.typeOf(expr);
@@ -1,6 +1,6 @@
import * as ts from "../../ts7/adapter.js";
import { DYN, JSVAL, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js";
import type { IrExpr, SrcLoc } from "../../../ir/ir.js";
import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js";
import { isNodeEsmFile, locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
@@ -168,6 +168,39 @@ export function lowerOptionalChain(lowerer: Lowerer, expr: ts.CallExpression | t
const loweredReceiver = lowerer.lowerExpr(recvNode);
const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
if (receiver.type.kind === "dyn") {
if (dotNode === expr && ts.isCallExpression(expr)) {
const id = `chain.${lowerer.chainCounter++}`;
const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc };
const stmts: IrStmt[] = [];
let guarded = receiver;
let thisValue: IrExpr | undefined;
// Member calls retain their receiver, evaluated before the guarded
// property read and only once, including effectful computed keys.
if (ts.isPropertyAccessExpression(recvNode) || ts.isElementAccessExpression(recvNode)) {
const object = lowerer.lowerExpr(recvNode.expression);
const local = lowerer.declareHiddenLocal("%optionalThis", object.type);
const reference: IrExpr = { kind: "varRef", localId: local.id, type: object.type, loc };
stmts.push({ kind: "varDecl", localId: local.id, init: object, loc });
thisValue = lowerer.coerceInto(recvNode.expression, reference, DYN);
const previous = lowerer.chainRecvByNode.get(recvNode.expression);
lowerer.chainRecvByNode.set(recvNode.expression, reference);
try { guarded = lowerer.lowerExpr(recvNode); }
finally {
if (previous) lowerer.chainRecvByNode.set(recvNode.expression, previous);
else lowerer.chainRecvByNode.delete(recvNode.expression);
}
}
const spreads: { arg: number; what: string }[] = [];
const args = expr.arguments.map((arg, index) => {
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
spreads.push({ arg: index, what: arg.expression.getText() });
return lowerer.lowerExprExpecting(arg.expression, DYN);
});
const body: IrExpr = { kind: "dynCall", callee, ...(thisValue ? { receiver: thisValue } : {}),
calleeName: recvNode.getText(), args, ...(spreads.length ? { spreads } : {}), type: DYN, loc };
const result: IrExpr = { kind: "optChain", id, receiver: guarded, body, type: DYN, loc };
return stmts.length ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
}
// `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
// represents undefined directly, so the chain step IS the keyed
// read with the optional (unit-answers-undefined) policy — no
@@ -41,6 +41,8 @@ export interface FnCtx {
* rest param): the synthetic trailing dyn-array param `arguments`
* reads resolve to. */
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
@@ -110,8 +112,9 @@ export function declareContextThis(ctx: FnCtx, type: IrType): IrLocal {
}
/** Look up a binding without allocating captures or boxing its source. */
export function bindingInContext(ctx: FnCtx, symbol: ts.Symbol | undefined): IrLocal | null {
export function bindingInContext(ctx: FnCtx, symbol: ts.Symbol | "arguments" | undefined): IrLocal | null {
if (symbol === undefined) return ctx.thisLocal;
if (symbol === "arguments") return ctx.argumentsLocal ?? null;
for (let i = ctx.scopes.length - 1; i >= 0; i--) {
const local = ctx.scopes[i]!.get(symbol);
if (local) return local;
@@ -122,7 +125,7 @@ export function bindingInContext(ctx: FnCtx, symbol: ts.Symbol | undefined): IrL
export interface ContextBindingResult {
local: IrLocal | null;
origin: IrLocal | null;
error: "caught" | "plain" | null;
error: "plain" | null;
}
/** Resolve a declared binding across function boundaries. Checker queries,
@@ -135,7 +138,7 @@ export interface ContextBindingResult {
* metadata to the same binding without duplicating the lookup algorithm. */
export function captureContextBinding(
stack: FnCtx[],
symbol: ts.Symbol | undefined,
symbol: ts.Symbol | "arguments" | undefined,
onCapture: (parent: IrLocal, child: IrLocal) => void,
): ContextBindingResult {
const current = stack[stack.length - 1];
@@ -145,22 +148,20 @@ export function captureContextBinding(
for (let depth = stack.length - 2; depth >= 0; depth--) {
const origin = bindingInContext(stack[depth]!, symbol);
if (origin === null) continue;
// Caught values are scoped exception storage. A typed local derived
// from the exception can escape; the catch binding itself cannot.
if (origin.type.kind === "caught") return { local: null, origin, error: "caught" };
// dyn/jsval retain their existing untraced box contract. Typed
// references use the backends' box tracing for capture cycles.
origin.boxed = true;
let parentEntry = origin;
for (let j = depth + 1; j < stack.length; j++) {
const ctx = stack[j]!;
let entry = symbol === undefined ? ctx.thisLocal : ctx.captureBySymbol.get(symbol);
let entry = symbol === undefined ? ctx.thisLocal : symbol === "arguments" ? ctx.argumentsLocal : ctx.captureBySymbol.get(symbol);
if (!entry) {
if (ctx.captures === null) return { local: null, origin, error: "plain" };
entry = declareContextLocal(ctx, origin.name, origin.type, origin.mutable, undefined, origin.source);
entry.boxed = true;
if (origin.tdz) entry.tdz = true;
if (symbol === undefined) ctx.thisLocal = entry;
else if (symbol === "arguments") ctx.argumentsLocal = entry;
else ctx.captureBySymbol.set(symbol, entry);
ctx.captures.push({ localId: entry.id, name: entry.name, type: entry.type });
ctx.captureSources.push(parentEntry.id);
@@ -172,3 +173,9 @@ export function captureContextBinding(
}
return { local: null, origin: null, error: null };
}
export function captureContextArguments(stack: FnCtx[]): IrLocal | null {
let owner = stack.length - 1;
while (owner > 0 && stack[owner]!.inheritsArguments) owner--;
return captureContextBinding(stack.slice(owner), "arguments", () => {}).local;
}
@@ -1,4 +1,4 @@
import { BOOL, DYN, STRING, canConvertToDyn, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { BOOL, DYN, STRING, canConvertToDyn, funcOf, typeEquals, typeKey, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js";
import { varRef } from "../../ir/build.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { genResultRecord } from "../type-mapper.js";
@@ -13,6 +13,7 @@ export class GeneratorDynamicDispatch {
private readonly generators = new Map<string, GeneratorType>();
private readonly dispatches = new Map<string, { fn: IrFunction; source: Invoke; types: Set<string> }>();
private readonly generated = new Set<IrFunction>();
private readonly properties = new Map<string, { fn: IrFunction; types: Set<string> }>();
process(lowerer: Lowerer, functions: readonly IrFunction[], classHelpers: ReadonlySet<IrFunction>): boolean {
const seen = new Set<string>();
@@ -31,6 +32,7 @@ export class GeneratorDynamicDispatch {
for (const fn of functions) everyStmtList(fn.body, { stmt: () => true, expr: (expr) => {
if (expr.kind === "dynFrom") discover(expr.value.type);
if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method)) rewrite.add(fn);
if (expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && ["next", "return", "throw"].includes(expr.key.value)) rewrite.add(fn);
return true;
} });
let changed = false;
@@ -40,6 +42,29 @@ export class GeneratorDynamicDispatch {
// to the generator helper and form an indirect recursion cycle.
if (this.generated.has(fn) || classHelpers.has(fn) || !rewrite.has(fn)) continue;
fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => {
if (expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && ["next", "return", "throw"].includes(expr.key.value)) {
const method = expr.key.value;
let property = this.properties.get(method);
if (!property) {
const loc = expr.loc;
const fn: IrFunction = { name: `%dyn.generator.get.${method}`,
params: [{ localId: "value", name: "value", type: DYN }], returnType: DYN,
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
body: [{ kind: "return", value: { ...expr, value: varRef("value", DYN, loc) }, loc }], loc };
property = { fn, types: new Set() };
this.properties.set(method, property);
this.generated.add(fn);
lowerer.liftedFns.push(fn);
lowerer.liftedFns.push({ name: `%dyn.generator.method.${method}`,
params: [{ localId: "arg", name: "arg", type: DYN }], returnType: DYN,
locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }],
body: [{ kind: "return", value: { kind: "dynInvoke", method, calleeName: `Generator.${method}`,
recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
args: [varRef("arg", DYN, loc)], type: DYN, loc }, loc }], loc });
changed = true;
}
return { kind: "call", callee: property.fn.name, args: [expr.value], type: DYN, loc: expr.loc };
}
if (expr.kind !== "dynInvoke" || !["next", "return", "throw"].includes(expr.method)) return expr;
const key = JSON.stringify([expr.method, expr.args.length, expr.calleeName]);
let dispatch = this.dispatches.get(key);
@@ -60,6 +85,16 @@ export class GeneratorDynamicDispatch {
return { kind: "call", callee: dispatch.fn.name, args: [expr.recv, ...expr.args], type: DYN, loc: expr.loc };
} });
}
for (const [method, property] of this.properties) for (const [key] of this.generators) {
if (property.types.has(key)) continue;
property.types.add(key);
const loc = property.fn.loc;
property.fn.body.unshift({ kind: "if", cond: { kind: "libCall", fn: "dyn.typedRefIs",
args: [varRef("value", DYN, loc), { kind: "strLit", value: key, type: STRING, loc }], type: BOOL, loc },
then: [{ kind: "return", value: { kind: "dynFrom", value: { kind: "closure", fnName: `%dyn.generator.method.${method}`,
captures: [], type: funcOf([DYN], DYN), loc }, type: DYN, loc }, loc }], else_: null, loc });
changed = true;
}
for (const dispatch of this.dispatches.values()) for (const [key, type] of this.generators) {
if (dispatch.types.has(key)) continue;
dispatch.types.add(key);
@@ -1,8 +1,26 @@
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, NULL_T, STRING, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
import { BOOL, DYN, F64, NULL_T, STRING, UNDEFINED_T, arrayOf, type IrExpr, type IrLibFn, type IrStmt, type SrcLoc } from "../../ir/ir.js";
import { strLit, varRef } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
import { BUILTIN_MODULE_CONSTS, BUILTIN_MODULE_FNS, OBJECT_CALLABLE_VALUES, builtinConstLit, builtinModuleConstOf, builtinModulesArrayLit } from "./surfaces.js";
import { BUILTIN_MODULE_CONSTS, BUILTIN_MODULE_FNS, OBJECT_CALLABLE_VALUES, builtinConstLit, builtinModuleConstOf, builtinModulesArrayLit, stdlibGlobalNameOf } from "./surfaces.js";
const PROTOTYPE_METHODS: Record<string, readonly string[]> = {
Date: ["getTime", "valueOf", "toISOString", "toJSON"],
Map: ["get", "set", "has", "delete", "clear", "keys", "values", "entries", "forEach"],
Set: ["add", "has", "delete", "clear", "keys", "values", "entries", "forEach"],
WeakMap: ["get", "set", "has", "delete"],
WeakSet: ["add", "has", "delete"],
String: ["split", "startsWith", "endsWith", "substring", "slice", "toLowerCase", "toUpperCase", "padStart", "charCodeAt", "normalize", "replace", "toString", "valueOf"],
Number: ["toString", "valueOf"],
Object: ["hasOwnProperty", "propertyIsEnumerable", "toString", "valueOf"],
};
export function builtinPrototypeMethod(lowerer: Lowerer, expr: ts.PropertyAccessExpression): string | null {
const prototype = expr.expression;
if (!ts.isPropertyAccessExpression(prototype) || prototype.name.text !== "prototype") return null;
const owner = stdlibGlobalNameOf(lowerer, prototype.expression);
return owner && PROTOTYPE_METHODS[owner]?.includes(expr.name.text) ? owner : null;
}
/** These native callables own their argument checks. Unannotated JS aliases
* keep the checked value instead of narrowing it to the ambient signature.
@@ -10,6 +28,11 @@ import { BUILTIN_MODULE_CONSTS, BUILTIN_MODULE_FNS, OBJECT_CALLABLE_VALUES, buil
export function isNativeBuiltinValueInitializer(lowerer: Lowerer, expr: ts.Expression | undefined, seen = new Set<ts.Symbol>()): boolean {
if (!expr) return false;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
// JS global snapshots may use an opaque builtin identity rather than
// the ambient constructor's callable ABI. Store that actual value in
// the checked representation, including optional capability probes.
const global = ts.isIdentifier(expr) ? stdlibGlobalNameOf(lowerer, expr) : null;
if (global && !["undefined", "Infinity", "NaN"].includes(global)) return true;
if (lowerer.isStdlibGlobal(expr, "console")) return true;
if (ts.isNewExpression(expr) && lowerer.isStdlibGlobal(expr.expression, "Date")) return true;
if (ts.isConditionalExpression(expr)) {
@@ -17,14 +40,24 @@ export function isNativeBuiltinValueInitializer(lowerer: Lowerer, expr: ts.Expre
isNativeBuiltinValueInitializer(lowerer, expr.whenFalse, new Set(seen));
}
if (ts.isPropertyAccessExpression(expr)) {
return lowerer.stdlibGlobalMember(expr, "console") !== null ||
return builtinPrototypeMethod(lowerer, expr) !== null ||
lowerer.stdlibGlobalMember(expr, "console") !== null ||
lowerer.stdlibGlobalMember(expr, "globalThis") === "console" ||
lowerer.stdlibGlobalMember(expr, "process") === "getBuiltinModule" ||
lowerer.stdlibGlobalMember(expr, "process") === "hrtime" ||
(expr.name.text === "bigint" && ts.isPropertyAccessExpression(expr.expression) &&
lowerer.stdlibGlobalMember(expr.expression, "process") === "hrtime") ||
lowerer.stdlibGlobalMember(expr, "Array") === "isArray" ||
lowerer.stdlibGlobalMember(expr, "Array") === "from" ||
lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys" ||
["fromCharCode", "fromCodePoint"].includes(lowerer.stdlibGlobalMember(expr, "String") ?? "") ||
["parseInt", "parseFloat"].includes(lowerer.stdlibGlobalMember(expr, "Number") ?? "") ||
lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer" ||
lowerer.stdlibGlobalMember(expr, "Array") === "prototype" ||
(ts.isPropertyAccessExpression(expr.expression) && lowerer.stdlibGlobalMember(expr.expression, "Array") === "prototype") ||
lowerer.stdlibGlobalMember(expr, "Object") === "assign" ||
lowerer.stdlibGlobalMember(expr, "Object") === "create" ||
(expr.name.text === "apply" && ts.isPropertyAccessExpression(expr.expression) && lowerer.stdlibGlobalMember(expr.expression, "Function") === "prototype") ||
lowerer.stdlibGlobalMember(expr, "JSON") === "stringify" ||
(lowerer.isStdlibGlobal(expr.expression, "Object") && Object.hasOwn(OBJECT_CALLABLE_VALUES, expr.name.text));
}
@@ -53,6 +86,58 @@ export function lowerArrayIsArrayValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
return { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: [DYN], ret: BOOL }, loc };
}
export function lowerStringCodesValue(lowerer: Lowerer, member: "fromCharCode" | "fromCodePoint", loc: SrcLoc): IrExpr {
const name = `%builtin.String.${member}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const values = varRef("values", DYN, loc), index = varRef("index", F64, loc), codes = varRef("codes", arrayOf(F64), loc), code = varRef("code", F64, loc);
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
lowerer.liftedFns.push({ name, params: [{ localId: "first", name: "first", type: DYN }, { localId: "values", name: "values", type: DYN }], returnType: STRING,
locals: [{ id: "first", name: "first", type: DYN, mutable: false }, { id: "values", name: "values", type: DYN, mutable: false },
{ id: "codes", name: "codes", type: arrayOf(F64), mutable: false }, { id: "index", name: "index", type: F64, mutable: true }, { id: "code", name: "code", type: F64, mutable: false }], loc,
body: [
{ kind: "varDecl", localId: "codes", init: { kind: "arrayLit", elems: [], type: arrayOf(F64), loc }, loc },
{ kind: "varDecl", localId: "index", init: number(0), loc },
{ kind: "while", cond: { kind: "bin", op: "<", left: index, right: { kind: "dynCheck", value: { kind: "dynKeyGet", value: values, key: strLit("length", loc), type: DYN, loc }, type: F64, loc }, type: BOOL, loc }, loc, body: [
{ kind: "varDecl", localId: "code", init: { kind: "libCall", fn: "dyn.toNumberCoerce", args: [{ kind: "dynKeyGet", value: values, key: lowerer.coerceToExpected(index, DYN), type: DYN, loc }], type: F64, loc }, loc },
...(member === "fromCodePoint" ? [{ kind: "exprStmt" as const, expr: { kind: "libCall" as const, fn: "string.fromCodePoint" as const,
args: [{ kind: "arrayLit" as const, elems: [code], type: arrayOf(F64), loc }], type: STRING, loc }, loc }] : []),
{ kind: "exprStmt", expr: { kind: "arrIntrinsic", method: "push", receiver: codes, args: [code], type: F64, loc }, loc },
{ kind: "assign", localId: "index", value: { kind: "bin", op: "+", left: index, right: number(1), type: F64, loc }, loc },
] },
{ kind: "return", value: { kind: "libCall", fn: `string.${member}`, args: [codes], type: STRING, loc }, loc },
],
});
}
return { kind: "dynFrom", value: { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: [DYN], ret: STRING, rest: true, argumentsAll: true }, loc }, fnName: member, type: DYN, loc };
}
export function lowerCheckedPredicateValue(lowerer: Lowerer, display: string, test: "number" | "buffer", loc: SrcLoc, predicate?: IrLibFn): IrExpr {
const name = `%builtin.${display}`;
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const value = varRef("value", DYN, loc);
const matches: IrExpr = { kind: "dynTest", test, value, type: BOOL, loc };
const result: IrExpr = predicate ? { kind: "ternary", cond: matches,
then: { kind: "libCall", fn: predicate, args: [{ kind: "dynCheck", value, type: F64, loc }], type: BOOL, loc },
else_: { kind: "boolLit", value: false, type: BOOL, loc }, type: BOOL, loc } : matches;
lowerer.liftedFns.push({ name, params: [{ localId: "value", name: "value", type: DYN }], returnType: BOOL,
locals: [{ id: "value", name: "value", type: DYN, mutable: false }], body: [{ kind: "return", value: result, loc }], loc });
}
return { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: [DYN], ret: BOOL }, loc };
}
export function lowerNumberParserValue(lowerer: Lowerer, member: "parseInt" | "parseFloat", loc: SrcLoc): IrExpr {
const name = `%builtin.Number.${member}`;
const types = member === "parseInt" ? [DYN, DYN] : [DYN];
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const params = types.map((type, index) => ({ localId: `p.${index}`, name: `p${index}`, type }));
const text: IrExpr = { kind: "libCall", fn: "dyn.toStringCoerce", args: [varRef("p.0", DYN, loc)], type: STRING, loc };
const radix: IrExpr = { kind: "libCall", fn: "dyn.toNumberCoerce", args: [varRef("p.1", DYN, loc)], type: F64, loc };
lowerer.liftedFns.push({ name, params, returnType: F64, locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
body: [{ kind: "return", value: { kind: "libCall", fn: `num.${member}`, args: member === "parseInt" ? [text, radix] : [text], type: F64, loc }, loc }], loc });
}
return { kind: "dynFrom", value: { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: types, ret: F64 }, loc }, fnName: member, type: DYN, loc };
}
// These module values expose existing native callable lowerings and main-thread
// metadata. Other exports keep an explicit runtime refusal, including Worker.
const MODULES = ["path/posix", "path/win32", "os", "worker_threads"] as const;
@@ -104,6 +189,17 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr {
/** A first-class loader uses the same native functions as direct imports.
* Module handles cache values and preserve aliases without a JS engine. */
export function lowerObjectAssignValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const name = "%builtin.Object.assign";
if (!lowerer.liftedFns.some((fn) => fn.name === name)) lowerer.liftedFns.push({
name, params: [{ localId: "target", name: "target", type: DYN }, { localId: "sources", name: "sources", type: DYN }],
returnType: DYN,
locals: [{ id: "target", name: "target", type: DYN, mutable: false }, { id: "sources", name: "sources", type: DYN, mutable: false }],
body: [{ kind: "return", value: { kind: "libCall", fn: "dyn.assignAll", args: [varRef("target", DYN, loc), varRef("sources", DYN, loc)], type: DYN, loc }, loc }], loc,
});
return { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: [DYN], ret: DYN, rest: true }, loc };
}
export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const cacheKey = "%builtin.process.getBuiltinModule";
let name = lowerer.builtinCallableValueFns.get(cacheKey);
@@ -8,6 +8,7 @@ import { builtinModules } from "node:module";
import { dirname, isAbsolute, resolve } from "node:path";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { lowerCheckedPredicateValue } from "./lower-builtin-values.js";
import { PoisonError, dynUndefinedExpr, ladderFenceExpr, nodeThrowExpr, own } from "./lowerer.js";
import { canonicalBuiltinModule, isCreateRequireBinding7, isJsSourceFile, isNodeEsmFile, locOf, npmStaticDepSf7, requireSpecOf, resolveImport } from "../program.js";
import { isRelativeSpecifier } from "../workspace-registry.js";
@@ -511,7 +512,7 @@ export function isNativeFfiRequire(lowerer: Lowerer, expr: ts.Expression | undef
if (!ts.isCallExpression(call)) return null;
const cr = createRequireSpecOf(lowerer, call);
if (cr === null || cr.spec === null || canonicalBuiltinModule(cr.spec) !== null) return null;
const dep = resolveImport(lowerer.program, cr.baseFile, cr.spec) ??
const dep = resolveImport(lowerer.program, cr.baseFile, cr.spec, "require") ??
npmStaticDepSf7(lowerer.program, cr.baseFile, cr.spec, "require");
if (dep === null || dep.fileName.endsWith(".json")) return null;
return { spec: cr.spec, baseFile: cr.baseFile, dep };
@@ -2610,16 +2611,21 @@ function lowerFsSyncBufferWindow(
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
}
/** Reflect.apply(target, thisArg, argsList) where the TARGET is a builtin
/** Reflect.ownKeys uses native computed-key reflection. Reflect.apply(target,
* thisArg, argsList) where the TARGET is a builtin
* rest-parameter table fn (path.join / path.resolve — test/common
* fixtures.js's fixturesPath forwards its rest args exactly this way):
* the packed libCall over a validated string[] extraction of argsList.
* The builtins ignore the receiver, so thisArg must be an effect-free
* spelling (`this`, an identifier, a unit literal) whose dropped
* evaluation is unobservable; every other Reflect.apply keeps the
* fence. Null when this isn't a Reflect.apply call. */
export function lowerReflectApplyCall(lowerer: Lowerer, call: ts.CallExpression,
* fence. Null when this isn't a supported Reflect call. */
export function lowerReflectCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
if (!call.questionDotToken && !access.questionDotToken && lowerer.stdlibGlobalMember(access, "Reflect") === "ownKeys" &&
call.arguments.length === 1 && !ts.isSpreadElement(call.arguments[0]!)) {
return { kind: "libCall", fn: "dyn.ownKeys", args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], type: DYN, loc: locOf(call) };
}
if (call.questionDotToken) return null;
if (lowerer.stdlibGlobalMember(access, "Reflect") !== "apply") return null;
const loc = locOf(call);
@@ -4887,6 +4893,18 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
return { kind: "libCall", fn: "json.parseReviver", args: [text, callback], type: DYN, loc };
}
if (member === "stringify") {
const replacerNode = call.arguments[1];
const spaceNode = call.arguments[2];
const runtimeOptions = replacerNode && !jsonNullishArgument(lowerer, replacerNode) &&
lowerer.mapTypeOf(lowerer.typeOf(replacerNode))?.kind === "dyn" ||
spaceNode && !jsonNullishArgument(lowerer, spaceNode) &&
!ts.isNumericLiteral(spaceNode) && !ts.isStringLiteral(spaceNode) && !ts.isNoSubstitutionTemplateLiteral(spaceNode) &&
!(ts.isPrefixUnaryExpression(spaceNode) && ts.isNumericLiteral(spaceNode.operand));
if (runtimeOptions && call.arguments[0]) {
const args = [0, 1, 2].map((index) => call.arguments[index]
? lowerer.lowerExprExpecting(call.arguments[index]!, DYN) : dynUndefinedExpr(loc));
return { kind: "libCall", fn: "json.stringifyValue", args, type: DYN, loc };
}
const indent = stringifySpaceIndent(lowerer, call);
if (call.arguments.length === 0) {
return lowerer.wrappedUndefined(lowerer.withUndefinedArm(STRING), loc)!;
@@ -5098,6 +5116,10 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
* refuse signatures that request one until that protocol is implemented. */
function lowerJsonCallback(lowerer: Lowerer, node: ts.Expression, role: "replacer" | "reviver"): IrExpr {
const callback = lowerer.lowerExpr(node);
if (callback.type.kind === "dyn") return callback;
if (callback.type.kind === "union" && lowerer.dynConvertible(callback.type) &&
lowerer.unions.get(callback.type.unionId)?.arms.every((arm) => isUnitType(arm) || arm.kind === "func" &&
arm.params.length <= 2 && (role !== "reviver" || (!arm.rest && !arm.argumentsAll)))) return lowerer.coerceToExpected(callback, DYN);
if (callback.type.kind === "func" && callback.type.params.length <= 2 &&
(role !== "reviver" || (!callback.type.rest && !callback.type.argumentsAll)) &&
canBoxFuncIntoDyn(callback.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
@@ -9513,11 +9535,12 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc
return { kind: "intrinsic", name: "promise.resolve", args: [], type: { kind: "promise", inner: VOID }, loc };
}
if (resultT.inner.kind === "void") {
// Promise.resolve(expr) at a void-promise type: the argument's
// effects must still run — no statement slot exists here for
// them, so only effect-free spellings could drop it honestly;
// fence rather than model that corner.
lowerer.noLowering("Promise.resolve with an argument at a void-promise type", call);
return {
kind: "seqExpr",
stmts: [lowerer.lowerExprStatement(argNode)],
result: { kind: "intrinsic", name: "promise.resolve", args: [], type: resultT, loc },
type: resultT, loc,
};
}
if (isUnitType(resultT.inner)) {
// A unit payload (Promise<undefined>/Promise<null>) has no
@@ -9537,6 +9560,8 @@ function lowerStoredTextCodecCall(lowerer: Lowerer, call: ts.CallExpression, acc
export function lowerNumberStaticProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
const member = lowerer.stdlibGlobalMember(expr, "Number");
if (member === null) return null;
const predicate = own(NUMBER_STATIC_PREDICATES, member);
if (predicate) return lowerCheckedPredicateValue(lowerer, `Number.${member}`, "number", locOf(expr), predicate);
const value = own(NUMBER_CONSTANTS, member);
if (value === undefined) return null;
return { kind: "numLit", value, type: F64, loc: locOf(expr) };
@@ -16,7 +16,7 @@ import { BYTES_ELEMENT_NAME, BIGINT_T, BOOL, CAUGHT, DYN, F64, type IrExpr, type
import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js";
import { isJsSourceFile, isNodeEsmFile, locOf, requireSpecOf } from "../program.js";
import { genResultRecord, isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr, staticImportNamespaceType } from "./lowerer.js";
import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, neverTaintedJsType, newFnCtx, nodeThrowExpr, staticImportNamespaceType } from "./lowerer.js";
import { enforceLibBoundary } from "./lib-boundary.js";
import { NARROW_FIRST, STRING_INDEX_METHODS, STR_METHODS, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js";
import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
@@ -25,12 +25,13 @@ import { mixinFnShapeOf } from "./lower-mixins.js";
import { dynStringReceiver, lowerArrayConstructor, lowerArrayFromCall, lowerArrayOfCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js";
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 { createRequireSpecOf, lowerChildStreamMethodCall, lowerChildWriterMethodCall, lowerCreateRequireCall, lowerCryptoHashMethodCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerNodeModuleCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.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 { hasRuntimeStatics } from "./class-runtime-statics.js";
import { httpClientFnBindingOf, isStreamUndefCallExpr, lowerCompatReqStreamOptionalCall, lowerHttpClientFnCall } from "./lower-server.js";
import { EMITTER_API_MEMBERS, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, returnsOnlyThis, type ClassInfo } from "./lower-classes.js";
import { EMITTER_API_MEMBERS, exactClassOfReceiver, exactInstanceClassOf, findGenericMethodOn, lowerClassGenericMethodCall, lowerStaticMethodCall, returnsOnlyThis, storedClassValueType, type ClassInfo } from "./lower-classes.js";
import { classCallbackCall, isClassCallback } from "./class-callbacks.js";
import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js";
import { lowerConsoleInspectArg, lowerFormatCall } from "./lower-inspect.js";
@@ -264,6 +265,13 @@ export interface GenericInstance {
* - `...xs: T[]`: the ABI type is the array; call sites pack the surplus.
*/
export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape {
// Inferred JS array patterns describe the bindings, not an exact tuple
// contract. Consume the actual iterable, including surplus elements.
if (ts.isArrayBindingPattern(param.name) && isJsSourceFile(param.getSourceFile()) && !param.type &&
!/@(?:param|type)\b/.test(param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()))) {
if (param.dotDotDotToken) return { type: DYN, mode: "dynRest" };
return param.initializer ? { type: DYN, mode: "omittable", bodyType: DYN } : { type: DYN, mode: "required" };
}
if (lowerer.checkedCallbackParams.has(param)) {
return param.initializer
? { type: DYN, mode: "omittable", bodyType: DYN }
@@ -346,7 +354,7 @@ export interface GenericInstance {
}
const type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name));
if (type.kind === "void") {
lowerer.unsupported("SC1090", param, "function parameters with void type");
return { type: DYN, mode: param.questionToken ? "omittable" : "required" };
}
if (param.questionToken && !lowerer.bareUndefinedArmedUnion(type) && type.kind !== "dyn" && type.kind !== "jsval") {
// `x?: unknown` where unknown came from an annotation: undefined is
@@ -1130,10 +1138,13 @@ function completeFuncValueArgs(
// that boundary; their factories keep the native record ABI.
const asyncReturn = decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && mappedReturn?.kind === "promise";
const recordReturn = asyncReturn ? mappedReturn.inner : mappedReturn;
const recordArms = recordReturn?.kind === "union" ? lowerer.unions.get(recordReturn.unionId)?.arms : recordReturn ? [recordReturn] : undefined;
const inferredRecordReturn = recordArms?.some((arm) => arm.kind === "record" || arm.kind === "object") &&
recordArms.every((arm) => isUnitType(arm) || arm.kind === "record" || arm.kind === "object" && !hasExplicitJsDocReturn(decl));
if (
isJsSourceFile(decl.getSourceFile()) &&
decl.type === undefined &&
recordReturn?.kind === "record" &&
recordReturn !== null && recordReturn !== undefined && inferredRecordReturn &&
lowerer.dynConvertible(recordReturn) &&
decl.parameters.every((p) => {
const mt = lowerer.mapTypeOf(lowerer.typeOf(p));
@@ -1792,6 +1803,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
body.push(...lowerer.lowerStmts(bodyBlock.statements));
if (fnCtx.inferReturn) {
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
inst.returnType = bodyReturn;
}
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl));
} else if (ts.isArrowFunction(decl) && decl.body !== undefined && !ts.isBlock(decl.body)) {
@@ -1804,12 +1816,14 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
if (value.type.kind === "void") {
body.push({ kind: "exprStmt", expr: value, loc: locOf(decl.body) });
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
inst.returnType = bodyReturn;
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl));
} else {
const stmt: IrStmt = { kind: "return", value, loc: locOf(decl.body) };
fnCtx.inferReturn.entries.push({ stmt, node: decl.body });
body.push(stmt);
bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl);
inst.returnType = bodyReturn;
}
} else {
const value = lowerer.lowerExprExpecting(decl.body, bodyReturn);
@@ -1859,8 +1873,10 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
* adds JS's undefined; DYN when returns disagree (the checked-dynamic
* result slot — today's shape). Same-key recursion PINNED the fallback
* type mid-lowering (callers already hold it), so a pinned instance
* keeps it and the wrap pass coerces every return to the pin. */
function resolveInferredReturn(lowerer: Lowerer, inst: GenericInstance,
* keeps it and the wrap pass coerces every return to the pin. Callers
* store the settled type on their full instance; this narrower view
* can be a structural copy in native builds. */
function resolveInferredReturn(lowerer: Lowerer, inst: Pick<GenericInstance, "returnType" | "returnPinned">,
infer: NonNullable<import("./lowerer.js").FnCtx["inferReturn"]>,
body: IrStmt[],
decl: ts.Node,): IrType {
@@ -1890,7 +1906,6 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
} else {
final = DYN; // disagreeing returns: the checked-dynamic join
}
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
@@ -2282,6 +2297,13 @@ function runtimeOptionalHofGenericBinding(
if (elem && (elem.flags & ts.TypeFlags.Any) !== 0) return null;
}
const mapped = lowerer.mapTypeOf(retTs);
// A JavaScript result inferred from an initial record can later
// carry additional fields. Settle its representation from the
// lowered returns instead of copying it back into that initial shape.
const resultArms = mapped?.kind === "union" ? lowerer.unions.get(mapped.unionId)?.arms : mapped ? [mapped] : [];
if (resultArms?.some((arm) => arm.kind === "record" || arm.kind === "object" || arm.kind === "classval") &&
resultArms.every((arm) => isUnitType(arm) || arm.kind === "record" || arm.kind === "object" || arm.kind === "classval") &&
!hasExplicitJsDocReturn(info.decl)) return null;
// Inferred variadic results carry runtime argument packs. Their
// factory must retain the lowered callable rather than extracting a
// fixed native signature from a checked value.
@@ -2325,6 +2347,7 @@ export function implicitMethodCallInfersReturn(lowerer: Lowerer, call: ts.CallEx
* checker's inferred shape. Preserve that boundary when specializing JS. */
function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null {
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
if (arg.kind === ts.SyntaxKind.ThisKeyword) return lowerer.resolveThis()?.type ?? null;
if (ts.isAwaitExpression(arg)) {
const awaited = storedImplicitArgumentType(lowerer, arg.expression);
return awaited?.kind === "promise" ? awaited.inner : awaited;
@@ -2383,7 +2406,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
// bound param forwarded into another implicit call transitively
// instantiates it (this._initCommandGroup(command)).
const t = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(arg));
const mapped = lowerer.mapTypeOf(t) ?? dynFallbackType(lowerer, arg, t);
const mapped = (neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ?? dynFallbackType(lowerer, arg, t);
// JavaScript arguments-based callables travel as checked functions.
// Specializing a parameter to the inferred rest signature would
// require extracting a native variadic ABI that the value does not
@@ -4050,8 +4073,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (name === "Boolean") return lowerer.lowerCondition(argNode);
if (name === "Number") return lowerNumberConstructorValue(lowerer, argNode, loc);
const arg = lowerer.lowerExpr(argNode);
if (name === "String" && arg.type.kind === "dyn") {
return { kind: "libCall", fn: "dyn.stringConstructor", args: [arg], type: STRING, loc };
if (name === "String" && (arg.type.kind === "dyn" || arg.type.kind === "union" &&
lowerer.unions.get(arg.type.unionId)?.arms.some((arm) => arm.kind === "symbol") && lowerer.dynConvertible(arg.type))) {
return { kind: "libCall", fn: "dyn.stringConstructor", args: [lowerer.coerceToExpected(arg, DYN)], type: STRING, loc };
}
if (name === "String") return lowerer.ensureString(arg, argNode);
}
@@ -4081,7 +4105,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.fenceStaticHeadersMember(access, "call");
lowerer.fenceStaticReadableStreamMember(access, "call");
const source = tryLowerExpression(lowerer, access.expression);
if (source && (source.type.kind === "dyn" || isDynTypedRefType(source.type) &&
if (source && (source.type.kind === "dyn" || source.type.kind === "func" ||
(source.type.kind === "map" || source.type.kind === "set") && lowerer.dynConvertible(source.type) || isDynTypedRefType(source.type) &&
lowerer.foldedStringKeyOf(access.argumentExpression) === null &&
lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression))?.kind !== "symbol")) {
const local = lowerer.declareHiddenLocal("%computedReceiver", DYN);
@@ -4815,7 +4840,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.lowerJsonMethodCall(expr, expr.expression) ??
// Reflect.apply of a builtin rest-parameter fn (fixtures.js's
// fixturesPath idiom) — before the stdlib member fence claims it.
lowerReflectApplyCall(lowerer, expr, expr.expression) ??
lowerReflectCall(lowerer, expr, expr.expression) ??
lowerer.lowerNumberStaticCall(expr, expr.expression) ??
lowerer.lowerDateCall(expr, expr.expression) ??
lowerer.lowerTextCodecCall(expr, expr.expression) ??
@@ -5688,8 +5713,9 @@ function lowerObjectOwnCall(
if (!isSafeToDiscard(effect)) stmts.push({ kind: "exprStmt", expr: effect, loc: locOf(extra) });
}
let key: IrExpr | null = keyRef;
if (receiverRef.type.kind === "dyn" && keyRef && (keyRef.type.kind === "symbol" || keyRef.type.kind === "dyn")) {
const result: IrExpr = { kind: "libCall", fn: enumerable ? "dyn.propertyIsEnumerableComputed" : "dyn.hasOwnComputed", args: [receiverRef, lowerer.coerceToExpected(keyRef, DYN)], type: BOOL, loc };
if (lowerer.dynConvertible(receiverRef.type) && keyRef && (receiverRef.type.kind === "object" || keyRef.type.kind === "symbol" || keyRef.type.kind === "dyn" ||
keyRef.type.kind === "union" && lowerer.dynConvertible(keyRef.type))) {
const result: IrExpr = { kind: "libCall", fn: enumerable ? "dyn.propertyIsEnumerableComputed" : "dyn.hasOwnComputed", args: [lowerer.coerceToExpected(receiverRef, DYN), lowerer.coerceToExpected(keyRef, DYN)], type: BOOL, loc };
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
}
if (!keyNode || rawKey?.type.kind === "undefinedT" || rawKey?.type.kind === "void") {
@@ -6164,6 +6190,12 @@ function lowerOptionalStringNumber(
if (probed?.type.kind !== "dyn") return null;
recv = probed;
}
// A factory may return a constructor more precise than its inferred
// checker result. Runtime static members live on that constructor.
if (recv.type.kind === "classval") {
const info = lowerer.classes.get(recv.type.className);
if (info && (info.callableBase || hasRuntimeStatics(info))) recv = lowerer.coerceToExpected(recv, DYN);
}
// A checker-`any` receiver that already lowered to a real STRING (the
// chained form — `cfg.host.trim().toLowerCase()`, where the first step
// extracted): no validation needed, ride the string tables directly.
@@ -6229,7 +6261,14 @@ function lowerOptionalStringNumber(
// unimplemented methods throw a LOUD not-supported Error; names the
// kind's prototype lacks throw Node's "x.y is not a function"; OBJ
// receivers call the own member.
const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, recv, arrayReceiver);
// A stored callback must be read before argument effects, and called
// with the original receiver rather than a materialized property view.
const callbackProperty = (() => {
const prop = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
return prop !== undefined && lowerer.checker.declarationsOf(prop).some((decl) =>
ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl));
})();
const dispatched = callbackProperty ? null : lowerDynDispatchMethodCall(lowerer, call, access, recv, arrayReceiver);
if (dispatched) return dispatched;
// Names NO dyn-representable prototype declares: the member can only
// be an OWN property, so "read the member, call it" IS Node's
@@ -6316,7 +6355,7 @@ const DYN_DISPATCH_METHODS = new Set([
"setEncoding", "setDefaultEncoding", "setTimeout", "read", "isPaused",
"writeHead", "setHeader", "getHeader", "hasHeader", "removeHeader",
"getHeaders", "getHeaderNames", "appendHeader", "flushHeaders",
"add", "append", "delete", "get", "getSetCookie", "has", "set",
"add", "append", "delete", "get", "getSetCookie", "has", "set", "clear", "keys", "values", "entries", "next",
"writeContinue", "writeEarlyHints", "cork", "uncork", "addTrailers",
"ref", "unref", "address", "setNoDelay", "setKeepAlive", "connect",
"resetAndDestroy", "destroySoon",
@@ -7423,14 +7462,19 @@ function loweredTemplateStrings(
* object-literal shorthand method to a module-level function and yields
* the `closure` expression creating it. */
export function lowerLambda(lowerer: Lowerer, node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,): IrExpr {
const prepared = (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) ? lowerer.preparedClassFactories.get(node) : undefined;
if (prepared) return prepared;
const loc = locOf(node);
const { shapes, funcType } = lowerer.lambdaSignature(node);
const signature = lowerer.lambdaSignature(node);
const { shapes } = signature;
let { funcType } = signature;
// A lambda IS a value: the completed-ABI rule applies at birth. The
// contextual (target) type decides — `(x?: number) => void` may flow
// into a slot annotated `(x: number | undefined) => void` (same ABI
// signature), anything else is fenced. Nested function declarations and
// object-literal shorthand methods aren't expressions — always fenced.
lowerer.requireExactArityValue(
if (!(ts.isMethodDeclaration(node) && isJsSourceFile(node.getSourceFile()) &&
canBoxFuncIntoDyn(funcType, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id)))) lowerer.requireExactArityValue(
node,
ts.isArrowFunction(node) || ts.isFunctionExpression(node) ? node : null,
shapes,
@@ -7463,11 +7507,18 @@ function loweredTemplateStrings(
isGenerator &&
(funcType.ret.kind !== "generator" || (funcType.ret.async === true) !== isAsync)
) lowerer.badType(node, lowerer.typeOf(node));
const bodyReturn = isGenerator
let bodyReturn = isGenerator
? lowerer.genBodyReturnType(funcType.ret)
: lowerer.bodyReturnType(isAsync, funcType.ret);
const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn);
const inferredSignature = isJsSourceFile(node.getSourceFile()) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) : undefined;
const returnsConstructor = inferredSignature && lowerer.checker.getConstructSignatures(lowerer.checker.getReturnTypeOfSignature(inferredSignature)).length > 0;
if (isJsSourceFile(node.getSourceFile()) && !node.type && !isAsync && !isGenerator &&
(bodyReturn.kind === "classval" || bodyReturn.kind === "dyn" && returnsConstructor)) {
fnCtx.inferReturn = { entries: [] };
}
fnCtx.inheritsArguments = ts.isArrowFunction(node);
fnCtx.isAsync = isAsync;
if (isGenerator && funcType.ret.kind === "generator") {
fnCtx.generator = generatorMeta(lowerer, funcType.ret);
@@ -7481,7 +7532,7 @@ function loweredTemplateStrings(
// and a method nested inside another method never captures its owner.
if (isJsSourceFile(node.getSourceFile()) && !ts.isArrowFunction(node)) {
const receiver = lowerer.declareThis(DYN);
prologue.unshift({ kind: "varDecl", localId: receiver.id, init: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, loc });
prologue.unshift({ kind: "varDecl", localId: receiver.id, init: { kind: "libCall", fn: isGenerator ? "dyn.generatorThis" : "dyn.this", args: [], type: DYN, loc }, loc });
}
// The synthetic trailing dyn-array param carries `arguments`.
// Parameterized functions receive all actual arguments; zero-param
@@ -7522,7 +7573,7 @@ function loweredTemplateStrings(
} else {
// Concise and block bodies use the same destination layout for
// fresh literals. Async bodies must inspect promises first.
let value = isAsync ? lowerer.lowerExpr(bodyExpr) : lowerer.lowerExprExpecting(bodyExpr, bodyReturn);
let value = isAsync || fnCtx.inferReturn ? lowerer.lowerExpr(bodyExpr) : lowerer.lowerExprExpecting(bodyExpr, bodyReturn);
// An async concise body whose value is itself a promise
// (`async () => p`): the async machinery RESOLVES the returned
// thenable into the function's own promise — lowerReturnValue's
@@ -7536,12 +7587,18 @@ function loweredTemplateStrings(
: [
{
kind: "return",
value: lowerer.coerceInto(bodyExpr, value, bodyReturn),
value: fnCtx.inferReturn ? value : lowerer.coerceInto(bodyExpr, value, bodyReturn),
loc: locOf(node.body!),
},
];
if (fnCtx.inferReturn && body[0]?.kind === "return") fnCtx.inferReturn.entries.push({ stmt: body[0], node: bodyExpr });
}
}
if (fnCtx.inferReturn) {
bodyReturn = resolveInferredReturn(lowerer, { returnType: bodyReturn }, fnCtx.inferReturn, body, node);
funcType = { ...funcType, ret: bodyReturn };
fnCtx.returnType = bodyReturn;
}
body = [...prologue, ...body];
// Bare-expression bodies never pass through lowerStmts, so the
// lib-boundary chokepoint runs here (idempotent for block bodies,
@@ -8035,7 +8092,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
);
}
const param = cb.type.params[0];
if (param !== undefined && !typeEquals(param, inner)) {
if (param !== undefined && !typeEquals(param, inner) && !(inner.kind === "void" && param.kind === "dyn")) {
lowerer.unsupported(
"SC1090",
call.arguments[0]!,
@@ -8076,6 +8133,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
handlerArgs = [{ kind: "varRef", localId: vLocal.id, type: inner, loc }];
} else {
body.push({ kind: "exprStmt", expr: awaitE, loc });
if (param !== undefined) handlerArgs = [dynUndefinedExpr(loc)];
}
const handlerCall: IrExpr = {
kind: "callValue",
@@ -9171,11 +9229,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const unitTest = lowerer.lowerUnitComparison(left, right, false, loc);
if (unitTest) return unitTest;
if (lk === "dyn" || rk === "dyn" || lk === "jsval" || rk === "jsval") {
lowerer.noLowering(
"Object.is over a dynamic operand",
call,
"validate/narrow the value first (strict equality's rule) — SameValue only differs from === on numbers (NaN, ±0)",
);
return { kind: "libCall", fn: "dyn.sameValue", args: [lowerer.coerceInto(leftNode, left, DYN), lowerer.coerceInto(rightNode, right, DYN)], type: BOOL, loc };
}
if (left.type.kind === "union" || right.type.kind === "union") {
const ut = left.type.kind === "union" ? left.type : (right.type as IrType & { kind: "union" });
@@ -9317,7 +9371,8 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
if (prototype) return prototype;
if (isJsSourceFile(call.getSourceFile()) && call.arguments.length > 0 && !call.arguments.some(ts.isSpreadElement)) {
const target = tryLowerExpression(lowerer, call.arguments[0]!);
if (target?.type.kind === "func" && lowerer.dynConvertible(target.type)) {
if (target && ((target.type.kind === "func" && lowerer.dynConvertible(target.type)) ||
(target.type.kind === "object" && lowerer.errorHierarchyClassOf(target.type.className)))) {
const loc = locOf(call);
return { kind: "libCall", fn: "dyn.assignAll", args: [lowerer.coerceToExpected(target, DYN),
{ kind: "dynArrLit", elems: call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc },
@@ -9490,6 +9545,13 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
(member === "getOwnPropertyDescriptor" && call.arguments.length === 2)) {
if (call.arguments.some((a) => ts.isSpreadElement(a))) return null;
let target = tryLowerExpression(lowerer, call.arguments[0]!);
const targetNode = call.arguments[0]!;
// The checker gives C.prototype the instance type, but its lowered
// value is the shared prototype table, not an instance to unbox.
const prototypeTarget = ts.isPropertyAccessExpression(targetNode) && targetNode.name.text === "prototype" &&
(exactClassOfReceiver(lowerer, targetNode.expression) !== null || storedClassValueType(lowerer, targetNode.expression)?.kind === "classval");
const declaredTarget = target?.type.kind === "dyn" && !prototypeTarget ? lowerer.mapTypeOf(lowerer.typeOf(targetNode)) : null;
if (target && declaredTarget && isDynTypedRefType(declaredTarget)) target = lowerer.coerceToExpected(target, declaredTarget);
if (target && member === "defineProperty") {
const native = lowerClassDataDescriptor(lowerer, call, member, target);
if (native) return native;
@@ -9499,11 +9561,10 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
if (descriptor) return descriptor;
target = lowerer.coerceToExpected(target, DYN);
}
// Error.cause owns a live data-property slot shared with its checked
// view. Other native fields do not yet synchronize descriptor edits.
// Error instances share a live property table with their checked view.
const keyNode = call.arguments[1]!;
if (target?.type.kind === "object" && target.type.className !== "%DOMException" && lowerer.errorHierarchyClassOf(target.type.className) &&
ts.isStringLiteral(keyNode) && keyNode.text === "cause") {
((ts.isStringLiteral(keyNode) && (keyNode.text === "cause" || keyNode.text === "message")) || lowerer.mapTypeOf(lowerer.typeOf(keyNode))?.kind === "symbol")) {
target = lowerer.coerceToExpected(target, DYN);
}
if (target?.type.kind === "func" && lowerer.dynConvertible(target.type)) {
@@ -9632,14 +9693,19 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const recvNode = call.arguments[0]!;
const keyNode = call.arguments[1]!;
const probed = tryLowerExpression(lowerer, recvNode);
const constructor = probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined;
// A CHECKED-DYNAMIC receiver (the JS file-scope object-literal
// identity story): the runtime dyn probe — OBJ member presence, ARR
// index bounds, Node's ToObject TypeError on nullish.
if (probed?.type.kind === "dyn" || (probed && isDynTypedRefType(probed.type)) || (probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) {
if (probed?.type.kind === "dyn" || (probed && isDynTypedRefType(probed.type)) ||
(probed?.type.kind === "object" && lowerer.errorHierarchyClassOf(probed.type.className)) ||
(probed && constructor && (constructor.callableBase || hasRuntimeStatics(constructor))) ||
(probed?.type.kind === "func" && lowerer.dynConvertible(probed.type))) {
const loc = locOf(call);
const receiver = lowerer.coerceToExpected(probed, DYN);
const rawKey = lowerer.lowerExpr(keyNode);
if (rawKey.type.kind === "dyn" || rawKey.type.kind === "symbol") return {
if (rawKey.type.kind === "dyn" || rawKey.type.kind === "symbol" ||
rawKey.type.kind === "union" && lowerer.dynConvertible(rawKey.type)) return {
kind: "libCall", fn: "dyn.hasOwnComputed", args: [receiver, lowerer.coerceToExpected(rawKey, DYN)], type: BOOL, loc,
};
const key = ownPropertyKey(lowerer, rawKey);
@@ -9799,7 +9865,6 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const recT = argIr;
const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc };
const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc };
lowerer.arrHofHelpers.set(key, helper);
const fn: IrFunction = {
name: helper,
params: [{ localId: "r.0", name: "r", type: recT }],
@@ -9826,6 +9891,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
const elemOf = (value: IrExpr, vt: IrType): IrExpr | null => {
if (!valueT) return null;
if (typeEquals(vt, valueT)) return value;
if (valueT.kind === "dyn" && lowerer.dynConvertible(vt)) return lowerer.coerceToExpected(value, DYN);
if (valueT.kind === "union" && vt.kind !== "union") {
const tag = lowerer.armTag(valueT.unionId, vt);
if (tag >= 0) {
@@ -9845,7 +9911,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
guardUndefTag = undefTag;
const arms = lowerer.unions.get(f.type.unionId)?.arms ?? [];
const others = arms.filter((a): boolean => a.kind !== "undefinedT");
if (typeEquals(f.type, valueT ?? f.type)) {
if (valueT?.kind === "dyn" || typeEquals(f.type, valueT ?? f.type)) {
// The field union IS the result union (single-field shapes):
// push the raw box — but then the undefined skip must NOT
// narrow. Handled below via vt === valueT.
@@ -9870,7 +9936,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
"cannot re-tag into the result element type — read the fields directly)",
);
}
} else if (!typeEquals(f.type, valueT ?? f.type)) {
} else if (valueT?.kind !== "dyn" && !typeEquals(f.type, valueT ?? f.type)) {
lowerer.unsupported(
"SC1090",
call,
@@ -9921,6 +9987,7 @@ export function lowerPromiseMethodCall(lowerer: Lowerer, call: ts.CallExpression
fn.body = body;
};
finalize();
lowerer.arrHofHelpers.set(key, helper);
lowerer.shapeOrderHelperFinalizers.push(finalize);
lowerer.liftedFns.push(fn);
}
@@ -32,6 +32,8 @@ 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";
import { checkedClassConstruction } from "./class-construction.js";
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
const body = member.body;
@@ -71,7 +73,44 @@ export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression
const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression);
if (stored?.type.kind === "classval") return stored.type;
}
return lowerer.mapTypeOf(lowerer.typeOf(expression));
const type = lowerer.typeOf(expression);
const mapped = lowerer.mapTypeOf(type);
if (mapped?.kind === "classval" || !ts.isCallExpression(expression)) return mapped;
// A checked call can erase its returned constructor's ABI while the
// checker still identifies the class declaration. Collect that class in
// its factory's lexical scope before choosing the derived native layout.
// Cache the module-level closure so its captures and lifted bodies are
// exactly the ones used when the declaration executes.
for (const signature of lowerer.checker.getConstructSignatures(type)) {
const declaration = lowerer.checker.signatureDeclaration(signature);
const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null;
if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue;
let factory: ts.ArrowFunction | ts.FunctionExpression | undefined;
for (let parent: ts.Node | undefined = cls.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
if (ts.isArrowFunction(parent) || ts.isFunctionExpression(parent)) factory = parent;
else if (ts.isFunctionLike(parent)) { factory = undefined; break; }
}
if (!factory || !isJsSourceFile(factory.getSourceFile()) || factory.typeParameters?.length ||
!ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory ||
!ts.isVariableDeclarationList(factory.parent.parent) || !(factory.parent.parent.flags & ts.NodeFlags.Const) ||
!ts.isVariableStatement(factory.parent.parent.parent) || !ts.isSourceFile(factory.parent.parent.parent.parent) ||
lowerer.preparingClassFactories.has(factory)) continue;
if (!lowerer.preparedClassFactories.has(factory)) {
lowerer.preparingClassFactories.add(factory);
const collecting = lowerer.collecting, sink = lowerer.diagSink;
lowerer.collecting = false;
lowerer.diagSink = null;
try { lowerer.preparedClassFactories.set(factory, lowerer.lowerLambda(factory)); }
finally {
lowerer.collecting = collecting;
lowerer.diagSink = sink;
lowerer.preparingClassFactories.delete(factory);
}
}
const resolved = lowerer.mapTypeOf(type);
if (resolved?.kind === "classval") return resolved;
}
return mapped;
}
export interface ClassMethodSignature {
@@ -84,7 +123,12 @@ export interface ClassMethodSignature {
export interface ClassInfo {
def: IrClassDef;
runtimeStatics?: readonly ts.ClassElement[];
runtimePrototypeMembers?: readonly ts.ClassElement[];
localPrototypeData?: true;
prototypeMethodValues?: Map<string, IrExpr>;
prototypeReflectionReady?: true;
prototypeReflectionRefusal?: string;
localClass?: {
context: FnCtx | null;
bodies: Map<string, IrFunction>;
@@ -433,6 +477,10 @@ export interface GenericClassInfo {
{ name: "%code", type: STRING },
{ name: "%cause", type: DYN },
{ name: "%causeEnumerable", type: BOOL },
{ name: "%messagePresent", type: BOOL },
{ name: "%messageEnumerable", type: BOOL },
{ name: "%namePresent", type: BOOL },
{ name: "%nameEnumerable", type: BOOL },
{ name: "%stackFrames", type: STRING },
{ name: "%stack", type: STRING },
],
@@ -1068,11 +1116,15 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// so the base's ClassInfo already exists here. An INSTANTIATION's
// base is its family (whose base is the declared one) — the heritage
// clause resolved when the family collected.
let base: ClassInfo | null = inst ? inst.family : mixin ? mixin.base : null;
const computedBase = lowerer.computedClassBases.get(lowerer.classNamer(decl));
let base: ClassInfo | null = inst ? inst.family : mixin ? mixin.base : computedBase?.classInfo ?? null;
let callableBase: ClassInfo["callableBase"];
const adoptCallableBase = (expression: ts.Expression): boolean => {
if (!isJsSourceFile(decl.getSourceFile()) || functionLocalClass(decl) ||
!ts.isClassDeclaration(decl) || familyMode || inst || mixin) return false;
if (familyMode || inst || mixin) return false;
if (lowerer.computedCallableBases.has(decl)) {
callableBase = { expression, constructorId: `%g.${className}.base`, prototypeId: `%g.${className}.basePrototype` };
return true;
}
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression));
const source = signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined;
if (!source || !isJsSourceFile(source.getSourceFile()) ||
@@ -1112,7 +1164,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
}
}
}
for (const clause of inst || mixin ? [] : (decl.heritageClauses ?? [])) {
for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses ?? [])) {
// `implements` is pure type-world: tsc checked the conformance and
// the clause erases — nothing about the runtime class changes.
// (Assigning an instance INTO an interface-typed slot is a separate
@@ -1238,6 +1290,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
}
}
if (!t || !ts.isIdentifier(t.expression)) {
if (t && adoptCallableBase(t.expression)) continue;
lowerer.unsupported("SC1090", clause, "extending computed expressions");
}
const symbol = lowerer.resolveValueSymbol(t.expression);
@@ -1317,7 +1370,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
}
}
if (base?.localClass && !functionLocalClass(decl)) lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function");
if (base?.localClass && !functionLocalClass(decl) && !computedBase) lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function");
if (lowerer.collectingExprClasses.has(decl)) {
for (let parent = base; parent; parent = parent.base) {
if (parent.builtinEmitter || parent.builtinStream !== undefined) {
@@ -1362,6 +1415,8 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
const staticFields: ClassInfo["staticFields"] = [];
const staticMethods: NonNullable<ClassInfo["staticMethods"]> = new Map();
const staticBlocks: ts.ClassStaticBlockDeclaration[] = [];
const runtimeStatics: ts.ClassElement[] = [];
const runtimePrototypeMembers: ts.ClassElement[] = [];
// GENERIC methods (own type parameters), instance and static: only
// the SYNTAX is checked here — parameter/return types mention the
// type parameters and cannot map yet; bodies lower per call-site
@@ -1409,6 +1464,17 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
});
};
for (const member of decl.members) {
if (functionLocalClass(decl) && isJsSourceFile(decl.getSourceFile()) &&
(ts.isClassStaticBlockDeclaration(member) || ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) {
runtimeStatics.push(member);
continue;
}
if (functionLocalClass(decl) && isJsSourceFile(decl.getSourceFile()) &&
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body &&
ts.isComputedPropertyName(member.name) && classMemberNameOf(lowerer, member.name) === null) {
runtimePrototypeMembers.push(member);
continue;
}
if (ts.isClassStaticBlockDeclaration(member)) {
// Statics live on the FAMILY (JS has one class, one static
// storage, however many instantiations exist) — instantiations
@@ -1746,6 +1812,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// as undefined on construction, verified), and reads/writes ride
// the ordinary undefined-armed union machinery.
let type = lowerer.irTypeOf(member.name);
// Unannotated JavaScript callback slots retain the original
// callable and its receiver instead of adapting its signature.
if (isJsSourceFile(member.getSourceFile()) && !member.type &&
!/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) &&
type.kind === "func" && lowerer.dynConvertible(type)) type = DYN;
// JS permits reads before a constructor's first assignment even
// when its inferred field type omits undefined.
if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") {
@@ -2055,6 +2126,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// `m(x: number | undefined)` interchange soundly in overrides.
const overridden = lowerer.findMethodOn(base, mName);
if (overridden && isJsSourceFile(member.getSourceFile())) {
// An unannotated JS override can return a checked value into
// the inherited native ABI, just as its untyped parameters do.
// Body lowering inserts the conversion at each return.
if (!member.type && ft.ret.kind === "dyn" && overridden.sig.ret.kind !== "dyn" &&
!overridden.sig.async && !overridden.sig.gen) ft.ret = overridden.sig.ret;
// Fluent overrides return the same native receiver. Keep the
// inherited return ABI and upcast `this` when lowering returns.
if (overridden.sig.ret.kind === "object" && base &&
@@ -2171,9 +2247,19 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
const isGet = ts.isGetAccessor(member);
// #private accessors collect as "get:#x"/"set:#x" — the same
// reserved spelling, one more unspellable segment.
if (!ts.isIdentifier(member.name) && !ts.isPrivateIdentifier(member.name)) {
const prop = classMemberNameOf(lowerer, member.name);
if (prop === null) {
lowerer.unsupported("SC1090", member, "computed accessor names");
}
if (ts.isComputedPropertyName(member.name)) {
const key = classSymbolKeyOf(lowerer, member.name.expression);
if (key) {
if ([...symbolMethods, ...symbolFields].some(([identity, name]) => name === prop && identity !== key.identity)) {
lowerer.unsupported("SC1090", member.name, `distinct symbol keys sharing the printable name '${symbolFieldDisplayName(prop)}' in one class`);
}
symbolMethods.set(key.identity, prop);
}
}
// ABSTRACT accessors are the abstract-method story with property
// syntax: body-less by definition, they enter `methods` (marked
// abstract) as their "get:x"/"set:x" halves — slot declaration
@@ -2181,9 +2267,8 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
const abstractAccessor =
ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true;
if (!member.body && !abstractAccessor) lowerer.unsupported("SC1090", member, "bodyless accessors");
const prop = member.name.text;
const mName = `${isGet ? "get" : "set"}:${prop}`;
if (fields.has(prop)) {
if (fields.has(prop) && !((prop === "message" || prop === "name") && base && (base.def.name === "%Error" || lowerer.isSubclassOf(base.def.name, "%Error")))) {
// tsc rejects field/accessor mixing (TS2610/2611); defensive.
lowerer.unsupported("SC1090", member.name, "accessors sharing a name with a field");
}
@@ -2586,8 +2671,14 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
const ctorParams: ParamShape[] = ctor && !mixinForwarding
? lowerer.paramShapes(ctor.parameters)
: (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : []));
if (ctor && !mixinForwarding && isJsSourceFile(ctor.getSourceFile()) &&
!ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest" || shape.mode === "islandRest") && bodyReadsArguments(ctor)) {
ctorParams.push({ type: DYN, mode: "arguments" });
}
const info: ClassInfo = {
...(runtimeStatics.length ? { runtimeStatics } : {}),
...(runtimePrototypeMembers.length ? { runtimePrototypeMembers } : {}),
def: {
name: className,
// The JS-observable .name (the class object's name string and
@@ -2683,7 +2774,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
}
if (base) base.subclasses.push(info);
lowerer.classes.set(className, info);
if (callableBase) {
if (computedBase && !functionLocalClass(decl)) {
info.def.baseValueGlobal = `%g.${className}.computedBase`;
lowerer.globalsList.push({ id: info.def.baseValueGlobal, name: `${className} base`, type: computedBase.value.type, mutable: false });
}
if (callableBase && !functionLocalClass(decl)) {
lowerer.globalsList.push(
{ id: callableBase.constructorId, name: `${className} base`, type: DYN, mutable: false },
{ id: callableBase.prototypeId, name: `${className} base prototype`, type: DYN, mutable: false },
@@ -2868,6 +2963,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
* field, like fieldInitStmts; a block lowers as the block statement it
* is, so its statements poison individually inside lowerStmts. */
export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt[] {
if (info.localClass) return [];
// Mixin instantiations lower their initializers under the
// instantiation's bindings/context (a no-op for everything else —
// generic FAMILIES own their statics and carry no genericInstance).
@@ -2880,16 +2976,31 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// initializers and static blocks (verified against Node — the
// decorated result is what `this`/the class name mean inside them).
const out: IrStmt[] = [...lowerClassDecoration(lowerer, info)];
const computedBase = info.decl ? lowerer.computedClassBases.get(lowerer.classNamer(info.decl)) : undefined;
if (computedBase && info.def.baseValueGlobal) {
const loc = computedBase.value.loc;
out.push({ kind: "assign", localId: info.def.baseValueGlobal, value: computedBase.value, loc });
out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [
lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN),
lowerer.coerceToExpected({ kind: "varRef", localId: info.def.baseValueGlobal, type: computedBase.value.type, loc }, DYN),
], type: DYN, loc }, loc });
}
if (info.callableBase) {
const { expression, constructorId, prototypeId } = info.callableBase;
const loc = locOf(expression);
out.push(
{ kind: "assign", localId: constructorId, value: lowerer.lowerExprExpecting(expression, DYN), loc },
{ kind: "assign", localId: constructorId, value: info.decl && lowerer.computedCallableBases.has(info.decl)
? lowerer.coerceToExpected(lowerer.computedCallableBases.get(info.decl)!, DYN)
: lowerer.lowerExprExpecting(expression, DYN), loc },
{ kind: "assign", localId: prototypeId, value: { kind: "libCall", fn: "dyn.classBasePrototype",
args: [{ kind: "varRef", localId: constructorId, type: DYN, loc }], type: DYN, loc }, loc },
);
const prototype = classPrototypeData(lowerer, info, loc);
if (prototype) out.push({ kind: "exprStmt", expr: prototype, loc });
out.push({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [
lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl!), DYN),
{ kind: "varRef", localId: constructorId, type: DYN, loc },
], type: DYN, loc }, loc });
}
type Item =
| { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] }
@@ -3240,6 +3351,11 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node): IrExpr {
const display = info.def.name.replace(/^%|^%m\d+\./, "");
fenceDecorationThrows(lowerer, info, blame);
if (info.localClass && ts.isIdentifier(blame)) {
const stored = lowerer.resolveLocal(blame);
if (stored) return lowerer.coerceToExpected({ kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) },
{ kind: "classval", className: info.def.name });
}
if (info.localClass && !ts.isClassExpression(blame) && !ts.isClassDeclaration(blame)) {
lowerer.unsupported("SC1090", blame, "named self references inside function-local classes");
}
@@ -3316,6 +3432,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
* fall through to the ordinary chain so the static fence or the
* generic member rejection names the site. */
export function lowerStaticFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (expr.name.text === "prototype") return null;
if (expr.questionDotToken) return null;
if (!ts.isIdentifier(expr.expression) && expr.expression.kind !== ts.SyntaxKind.ThisKeyword) return null;
const symbol = ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : null;
@@ -3337,6 +3454,8 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
if (info.classDecorators?.valueGlobalId !== undefined) return null;
const loc = locOf(expr);
const found = findStaticOn(lowerer, info, expr.name.text);
if (hasRuntimeStatics(info)) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc };
// A #private static resolves only through the DECLARING class's own
// name: in JS the brand lives on that one constructor object, so
// `D.#s` (a subclass receiver) throws Node's TypeError instead of
@@ -3370,6 +3489,8 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
if (expr.name.text === "name" && info.def.jsName !== undefined) {
return { kind: "strLit", value: info.def.jsName, type: STRING, loc };
}
if (info.callableBase) return { kind: "dynKeyGet", value: lowerer.coerceToExpected(classValueRef(lowerer, info, expr.expression), DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc };
const data = classStaticDataFor(lowerer, info, expr.name.text, loc);
if (data) return { kind: "dynKeyGet", value: data, key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc };
return null;
@@ -3429,13 +3550,26 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
}
if (local && (expr.typeParameters?.length ||
decoratorNodesOf(expr).length || expr.members.some((member) =>
ts.isClassStaticBlockDeclaration(member) || decoratorNodesOf(member).length ||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) ||
decoratorNodesOf(member).length ||
(!isJsSourceFile(expr.getSourceFile()) && (ts.isClassStaticBlockDeclaration(member) ||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) ||
(ts.isMethodDeclaration(member) && member.typeParameters?.length)))) {
lowerer.unsupported("SC1090", expr, "function-local classes with statics, decorators, or generic members");
}
lowerer.collectingExprClasses.add(expr);
try {
if (local && lowerer.fnStack.length > 0 && !lowerer.computedClassBases.has(lowerer.classNamer(expr))) {
const heritage = expr.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression;
if (heritage && (ts.isCallExpression(heritage) ||
ts.isIdentifier(heritage) && (lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null)) {
let value = lowerer.lowerExpr(heritage);
const known = value.type.kind === "dyn" ? storedClassValueType(lowerer, heritage) : null;
if (known?.kind === "classval") value = lowerer.coerceToExpected(value, known);
const base = value.type.kind === "classval" ? lowerer.classes.get(value.type.className) : undefined;
if (base) lowerer.computedClassBases.set(lowerer.classNamer(expr), { value, classInfo: base });
else if (value.type.kind === "dyn" || value.type.kind === "func") lowerer.computedCallableBases.set(expr, value);
}
}
lowerer.collectClassShapeInner(expr, namedEvaluationName(expr));
} finally {
lowerer.collectingExprClasses.delete(expr);
@@ -3531,15 +3665,34 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
info.def.localCaptures.push({ localId: "%base", name: "base", type: { kind: "classval", className: info.base.def.name } });
}
info.localClass.ready = true;
lowerer.onExprClassCollected?.(info);
} finally {
lowerer.fnStack.pop();
info.localClass.context = null;
}
}
const value = classValueRef(lowerer, info, expr);
if (info.localClass && info.callableBase && value.kind === "classRef") {
const loc = locOf(expr);
const base = lowerer.lowerExprExpecting(info.callableBase.expression, DYN);
const baseLocal = lowerer.declareHiddenLocal("%classBase", DYN);
const clsLocal = lowerer.declareHiddenLocal("%classValue", value.type);
const baseRef: IrExpr = { kind: "varRef", localId: baseLocal.id, type: DYN, loc };
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc };
info.localPrototypeData = true;
return initializeRuntimeStatics(lowerer, info, { kind: "seqExpr", stmts: [
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
{ kind: "varDecl", localId: clsLocal.id, init: { ...value, captures: info.localClass.captureSources }, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, baseRef], type: DYN, loc }, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classPrototype", args: [boxed,
{ kind: "libCall", fn: "dyn.classBasePrototype", args: [baseRef], type: DYN, loc },
], type: DYN, loc }, loc },
], result: clsRef, type: value.type, loc });
}
if (info.localClass?.baseExpression && info.base && value.kind === "classRef") {
const loc = locOf(expr);
const base = lowerer.lowerExpr(info.localClass.baseExpression);
const base = lowerer.lowerExprExpecting(info.localClass.baseExpression, { kind: "classval", className: info.base.def.name });
const baseLocal = lowerer.declareHiddenLocal("%classBase", base.type);
baseLocal.boxed = true;
const clsLocal = lowerer.declareHiddenLocal("%classValue", value.type);
@@ -3547,17 +3700,20 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!;
info.localPrototypeData = true;
return { kind: "seqExpr", stmts: [
return initializeRuntimeStatics(lowerer, info, { kind: "seqExpr", stmts: [
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
{ kind: "varDecl", localId: clsLocal.id, init: { ...value, captures: [...info.localClass.captureSources, baseLocal.id] }, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classInherit", args: [
{ kind: "dynFrom", value: clsRef, type: DYN, loc }, { kind: "dynFrom", value: baseRef, type: DYN, loc },
], type: DYN, loc }, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classPrototype", args: [
{ kind: "dynFrom", value: clsRef, type: DYN, loc }, basePrototype,
], type: DYN, loc }, loc },
], result: clsRef, type: value.type, loc };
], result: clsRef, type: value.type, loc });
}
return info.localClass && value.kind === "classRef"
return initializeRuntimeStatics(lowerer, info, info.localClass && value.kind === "classRef"
? { ...value, captures: info.localClass.captureSources }
: value;
: value);
}
/** The EXACT class a receiver expression is statically known to BE (not
@@ -3784,6 +3940,9 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
throughValue: boolean,
): IrExpr | null {
const loc = locOf(call);
if (info.callableBase || hasRuntimeStatics(info)) return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: access.name.text, calleeName: access.name.text,
args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc };
// #private statics: through-a-VALUE receiver the call devirtualizes
// exactly when the value can only BE the declaring class
// (classValueIsExactlyOwn — a slot that could hold a descendant at
@@ -3902,6 +4061,9 @@ function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpr
if (recvT?.kind !== "classval") return null;
const loc = locOf(expr);
const member = expr.name.text;
const info = lowerer.classes.get(recvT.className);
if (info && (info.callableBase || hasRuntimeStatics(info))) return { kind: "dynKeyGet", value: lowerer.lowerExprExpecting(expr.expression, DYN),
key: { kind: "strLit", value: member, type: STRING, loc }, type: DYN, loc };
// `X.name` reads the RUNTIME class object's stored name — the one
// genuinely dynamic member. It CONSUMES the receiver (no evaluation
// is discarded), so any receiver expression is fine here.
@@ -3910,6 +4072,7 @@ function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpr
if (recv.type.kind !== "classval") return null;
return { kind: "libCall", fn: "class.name", args: [recv], type: STRING, loc };
}
if (!info) return null;
if (
!ts.isIdentifier(expr.expression) &&
expr.expression.kind !== ts.SyntaxKind.ThisKeyword &&
@@ -3929,8 +4092,6 @@ function staticMethodForReceiver(lowerer: Lowerer, access: ts.PropertyAccessExpr
}
// The direct class-name spelling resolved in lowerStaticFieldRead;
// reaching here means the receiver is a classval-typed BINDING.
const info = lowerer.classes.get(recvT.className);
if (!info) return null;
const found = findStaticOn(lowerer, info, member);
if (!found) {
lowerer.unsupported(
@@ -4374,6 +4535,11 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
// defaults on entry, ahead of everything the body does.
const declared = lowerer.declareParams(info.ctor.parameters, info.ctorParams);
params.push(...declared.params);
if (info.ctorParams.at(-1)?.mode === "arguments") {
const local = lowerer.declareHiddenLocal("%arguments", DYN);
params.push({ localId: local.id, name: local.name, type: DYN });
lowerer.ctx.argumentsLocal = local;
}
body.push(...declared.prologue);
if (!ctorBase && !info.callableBase) {
// Node's base-class order: field initializers run at the start
@@ -4685,7 +4851,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null {
const className = info.def.name;
const thisType: IrType = { kind: "object", className };
const memberName = ts.isMethodDeclaration(fnLike) ? classMemberNameOf(lowerer, fnLike.name) : ts.isIdentifier(fnLike.name) || ts.isPrivateIdentifier(fnLike.name) ? fnLike.name.text : null;
const memberName = classMemberNameOf(lowerer, fnLike.name);
if (memberName === null) return null;
const mName = ts.isMethodDeclaration(fnLike)
? memberName
@@ -5018,7 +5184,15 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
out.push(...paramPropInitStmts(lowerer, info, thisLocal));
continue;
}
const args = forward !== undefined
const surplusSpread = forward === undefined && !base.builtinError && !base.builtinEmitter &&
base.ctorParams.length === 0 && superCall.arguments.some(ts.isSpreadElement);
if (surplusSpread) {
// A parameterless base still evaluates and iterates every
// supplied argument before its constructor starts.
const evaluated = lowerer.completeArgs(superCall.arguments, [{ mode: "dynRest", type: DYN }], locOf(stmt), stmt);
for (const value of evaluated) out.push({ kind: "exprStmt", expr: value, loc: locOf(stmt) });
}
const args = surplusSpread ? [] : forward !== undefined
? forward
: base.builtinError
? lowerer.errorConstructorArgs(superCall.arguments, locOf(stmt), stmt)
@@ -5072,7 +5246,10 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
};
if (info.callableBase) return {
kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.classSuper",
args: [{ kind: "varRef", localId: info.callableBase.constructorId, type: DYN, loc },
args: [info.localClass ? { kind: "libCall", fn: "dyn.getPrototype", args: [
{ kind: "dynFrom", value: { kind: "fieldGet", obj: thisRef, className: info.def.name,
field: `%classEnvironment:${info.def.name}`, type: { kind: "classval", className: info.def.name }, loc }, type: DYN, loc },
], type: DYN, loc } : { kind: "varRef", localId: info.callableBase.constructorId, type: DYN, loc },
lowerer.coerceToExpected(thisRef, DYN), args[0]!], type: VOID, loc }, loc,
};
if (base.builtinError) {
@@ -5395,6 +5572,10 @@ function lowerProgramClassNew(lowerer: Lowerer, expr: ts.NewExpression, declared
const args = lowerer.inheritsBuiltinErrorCtor(info)
? lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr)
: lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr);
for (let current: ClassInfo | null = info; current; current = current.base) {
if (current.def.baseValueGlobal) return { kind: "newValue", callee: classValueRef(lowerer, info, expr),
args, type: { kind: "object", className: info.def.name }, loc };
}
return {
kind: "new",
className: info.def.name,
@@ -5542,12 +5723,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
"constructing a decorated class through a qualified name (a replacing decorator rebinds the class name — construct through the bare name)",
);
}
if (info) return lowerProgramClassNew(lowerer, expr, info, loc);
lowerer.unsupported(
"SC1090",
expr,
`constructing '${expr.expression.name.text}' (a namespace member with no class lowering)`,
);
if (info && !info.localClass) return lowerProgramClassNew(lowerer, expr, info, loc);
// A namespace export can hold a class returned by a factory.
// Let the class-value path below preserve that constructor's
// lexical environment instead of requiring a class declaration.
}
// `new B.C()` where B is an AMBIENT namespace (fundule merges
// included): Node evaluates the callee first and throws
@@ -6361,13 +6540,10 @@ 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.
// Checked construction supports emitted classes and builtin typed arrays.
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 };
return checkedClassConstruction(lowerer, callee, (expr.arguments ?? []).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), loc);
}
lowerer.unsupported("SC1090", expr, "constructing values other than classes declared in the program");
}
@@ -15,7 +15,7 @@ import { islandPrimitiveExit, lowerDynDispatchMethodCall } from "./lower-calls.j
import { buildArraySortFn } from "./lower-array-sort.js";
import { arrayIndexPresent, arrayValueRead, arrayValueStore, arrayValueType, currentArrayIndexPresent } from "./array-values.js";
import { typeKey } from "../type-mapper.js";
import { type WidthLift, nodeThrowExpr } from "./lowerer.js";
import { type WidthLift, newFnCtx, nodeThrowExpr } from "./lowerer.js";
import { boolLit, countedFor, numLit, strLit, varRef } from "../../ir/build.js";
import { defaultAfterUndefined, lowerPositionArgument, lowerStaticallyUndefinedArgument, positionNumber } from "./optional-arguments.js";
import { lowerArrayCopyWithin, lowerArrayFill } from "./array-indexed-mutation.js";
@@ -162,13 +162,6 @@ function lowerArrayJoinSeparator(lowerer: Lowerer, node: ts.Expression | undefin
* directly from its IR return type. Fence element kinds ScrArr cannot hold
* before constructing that array type. */
function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: string, elem: IrType): void {
if (elem.kind === "dyn") {
lowerer.unsupported(
"SC1090",
node,
`${producer} with a callback returning 'unknown'-typed values (the result array has no static element type — annotate the callback's return)`,
);
}
if (!isSupportedArrayElem(elem)) {
lowerer.unsupported(
"SC1090",
@@ -625,7 +618,11 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
"concat of a bare inner array onto an array-of-arrays (JS would SPREAD it one level — wrap it: a.concat([inner]))",
);
}
const arg = lowerer.lowerExpr(argNode);
// Declaration-backed JS methods can return checked values even
// when their public signature names a concrete array. Validate
// that boundary before spreading the represented elements.
let arg = lowerer.lowerExpr(argNode);
if (arg.type.kind === "dyn") arg = lowerer.coerceInto(argNode, arg, argArrayType);
if (arg.type.kind !== "array") lowerer.badType(argNode, lowerer.typeOf(argNode));
shape.push("a");
args.push(arg);
@@ -695,6 +692,12 @@ function fenceProducedArrayElem(lowerer: Lowerer, node: ts.Node, producer: strin
return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: arrayValueType(lowerer, elem), loc };
}
if (name === "join") {
if (elem.kind === "dyn") {
const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN);
const args = call.arguments.length ? [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)] : [];
return lowerer.coerceToExpected({ kind: "dynInvoke", recv: receiver, method: "join",
calleeName: access.getText(), args, type: DYN, loc }, STRING);
}
// The ambient declares join on every Array<T> (a per-element-type
// interface split isn't expressible there), so string-convertible
// elements are enforced here: nested arrays would need JS's recursive
@@ -2156,8 +2159,7 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc:
* undefined unit arm (for REF elements that arm is the unit instance, the
* standard union machinery). The Last pair is the SAME loop walked
* backwards (`i = n - 1; i >= 0; i--`), exactly the es2023 spec's
* descending index walk. All require a bool-returning callback
* (JS's ToBoolean of arbitrary predicate results has no lowering). */
* descending index walk. Predicate results use JavaScript ToBoolean. */
function lowerArrayFindLikeCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,
method: "find" | "findIndex" | "findLast" | "findLastIndex" | "some" | "every",
@@ -2176,7 +2178,7 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc:
// returns the index read's `string | undefined`).
const fnRet = fnArg.type.ret;
const truthyRet =
fnRet.kind === "bool" || fnRet.kind === "f64" || fnRet.kind === "string" ||
fnRet.kind === "bool" || fnRet.kind === "f64" || fnRet.kind === "string" || fnRet.kind === "dyn" ||
(fnRet.kind === "union" &&
(lowerer.unions.get(fnRet.unionId)?.arms ?? []).every((a) => a.kind !== "dyn" && a.kind !== "caught" && a.kind !== "jsval"));
if (!truthyRet) lowerer.badType(argNode, lowerer.typeOf(argNode));
@@ -2228,10 +2230,11 @@ export function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc:
}
/** ToBoolean of a predicate result inside a synthesized HOF helper — the
* ensureBool subset the find-like path admits (bool through; f64/string/
* truthy-answerable unions via toBool). */
* ensureBool subset the find-like path admits (bool through; checked
* values via dynTest; f64/string/truthy-answerable unions via toBool). */
function predToBool(e: IrExpr): IrExpr {
if (e.type.kind === "bool") return e;
if (e.type.kind === "dyn") return { kind: "dynTest", test: "truthy", value: e, type: BOOL, loc: e.loc };
return { kind: "toBool", operand: e, type: BOOL, loc: e.loc };
}
@@ -3110,32 +3113,79 @@ export function lowerArrayOfCall(lowerer: Lowerer, call: ts.CallExpression,
/** Mapper-less checked Array.from: acquire once, then step through emitted
* property and call dispatch so native class iterators retain their methods. */
function lowerCheckedArrayFrom(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrExpr {
function lowerCheckedArrayFrom(lowerer: Lowerer, source: IrExpr, loc: SrcLoc, mapper?: IrExpr, receiver?: IrExpr): Extract<IrExpr, { kind: "seqExpr" }> {
const iterator = lowerer.declareHiddenLocal("%fromIterator", DYN);
const next = lowerer.declareHiddenLocal("%fromNext", DYN);
const step = lowerer.declareHiddenLocal("%fromStep", DYN);
const out = lowerer.declareHiddenLocal("%fromArray", DYN);
const done = lowerer.declareHiddenLocal("%fromDone", BOOL);
done.mutable = true;
const index = mapper ? lowerer.declareHiddenLocal("%fromIndex", F64) : null;
if (index) index.mutable = true;
const get = (value: IrExpr, key: string): IrExpr => ({ kind: "dynKeyGet", value, key: strLit(key, loc), type: DYN, loc });
const item = get(varRef(step.id, DYN, loc), "value");
const mapped: IrExpr = mapper && index ? { kind: "ternary", cond: { kind: "dynTest", test: "undefined", value: mapper, type: BOOL, loc },
then: item, else_: { kind: "dynCall", callee: mapper, ...(receiver ? { receiver } : {}), calleeName: "Array.from mapper", args: [item, lowerer.coerceToExpected(varRef(index.id, F64, loc), DYN)], type: DYN, loc }, type: DYN, loc } : item;
let append: IrStmt = { kind: "exprStmt", expr: { kind: "dynInvoke", recv: varRef(out.id, DYN, loc), method: "push", calleeName: "Array.from", args: [mapped], type: DYN, loc }, loc };
if (mapper) {
const error = lowerer.declareHiddenLocal("%fromError", CAUGHT);
const close = lowerer.declareHiddenLocal("%fromReturn", DYN);
append = { kind: "tryCatch", tryBody: [append], catchLocalId: error.id, finallyBody: null, loc, catchBody: [
{ kind: "tryCatch", catchLocalId: null, catchBody: [], finallyBody: null, loc, tryBody: [
{ kind: "varDecl", localId: close.id, init: get(varRef(iterator.id, DYN, loc), "return"), loc },
{ kind: "if", cond: { kind: "dynTest", test: "function", value: varRef(close.id, DYN, loc), type: BOOL, loc }, then: [
{ kind: "exprStmt", expr: { kind: "dynCall", callee: varRef(close.id, DYN, loc), receiver: varRef(iterator.id, DYN, loc), calleeName: "iterator.return", args: [], type: DYN, loc }, loc },
], else_: null, loc },
] },
{ kind: "rethrow", localId: error.id, loc },
] };
}
const stmts: IrStmt[] = [
...(mapper ? [{ kind: "if" as const, cond: { kind: "dynTest" as const, test: "undefined" as const, value: mapper, type: BOOL, loc }, then: [], else_: [
{ kind: "if" as const, cond: { kind: "dynTest" as const, test: "function" as const, value: mapper, type: BOOL, loc }, then: [], else_: [
{ kind: "exprStmt" as const, expr: nodeThrowExpr(1, "", "Array.from mapper is not a function", DYN, loc), loc },
], loc },
], loc }] : []),
{ kind: "varDecl", localId: iterator.id, init: { kind: "libCall", fn: "dyn.iteratorResult", args: [{ kind: "libCall", fn: "dyn.arrayFromIterator", args: [source], type: DYN, loc }], type: DYN, loc }, loc },
{ kind: "varDecl", localId: next.id, init: get(varRef(iterator.id, DYN, loc), "next"), loc },
{ kind: "varDecl", localId: out.id, init: { kind: "dynArrLit", elems: [], type: DYN, loc }, loc },
{ kind: "varDecl", localId: done.id, init: boolLit(false, loc), loc },
...(index ? [{ kind: "varDecl" as const, localId: index.id, init: numLit(0, loc), loc }] : []),
{ kind: "while", cond: { kind: "unary", op: "!", operand: varRef(done.id, BOOL, loc), type: BOOL, loc }, body: [
{ kind: "varDecl", localId: step.id, init: { kind: "libCall", fn: "dyn.iteratorResult", args: [{
kind: "dynCall", callee: varRef(next.id, DYN, loc), receiver: varRef(iterator.id, DYN, loc), calleeName: "iterator.next", args: [], type: DYN, loc,
}], type: DYN, loc }, loc },
{ kind: "assign", localId: done.id, value: { kind: "dynTest", test: "truthy", value: get(varRef(step.id, DYN, loc), "done"), type: BOOL, loc }, loc },
{ kind: "if", cond: { kind: "unary", op: "!", operand: varRef(done.id, BOOL, loc), type: BOOL, loc }, then: [
{ kind: "exprStmt", expr: { kind: "dynInvoke", recv: varRef(out.id, DYN, loc), method: "push", calleeName: "Array.from", args: [get(varRef(step.id, DYN, loc), "value")], type: DYN, loc }, loc },
append,
...(index ? [{ kind: "assign" as const, localId: index.id, value: { kind: "bin" as const, op: "+" as const, left: varRef(index.id, F64, loc), right: numLit(1, loc), type: F64, loc }, loc }] : []),
], else_: null, loc },
], loc },
];
return { kind: "seqExpr", stmts, result: varRef(out.id, DYN, loc), type: DYN, loc };
}
export function lowerArrayFromValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const name = "%builtin.Array.from";
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const context = newFnCtx(false, null, null, DYN);
lowerer.fnStack.push(context);
try {
const params = ["source", "options"].map((name) => {
const local = lowerer.declareHiddenLocal(name, DYN);
return { localId: local.id, name, type: DYN };
});
const [source, options] = params.map((p) => varRef(p.localId, DYN, loc));
const argument = (index: string): IrExpr => ({ kind: "dynKeyGet", value: options!, key: strLit(index, loc), type: DYN, loc });
const value = lowerCheckedArrayFrom(lowerer, source!, loc, argument("0"), argument("1"));
const constructorCheck: IrStmt = { kind: "if", cond: { kind: "dynTest", test: "function", value: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, type: BOOL, loc },
then: [{ kind: "runtimeFence", code: "SC2020", message: "Array.from with a custom constructor receiver is not supported", loc }], else_: null, loc };
lowerer.liftedFns.push({ name, params, returnType: DYN, locals: context.locals, body: [constructorCheck, ...value.stmts, { kind: "return", value: value.result, loc }], loc });
} finally { lowerer.fnStack.pop(); }
}
return { kind: "dynFrom", value: { kind: "closure", fnName: name, captures: [], type: { kind: "func", params: [DYN], ret: DYN, rest: true }, loc }, fnName: "from", type: DYN, loc };
}
/** `Array.from({ length: n }, mapfn)` — the counted-generation idiom — on
* THE stdlib Array global. The source must be an OBJECT LITERAL whose
* single property is `length` (the shape the idiom always spells; the
@@ -5500,23 +5550,12 @@ const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every",
binds.push({ kind: "varDecl", localId: vLocal.id, init: secondRead(), loc });
}
} else {
// Identifier heads and the remaining patterns bind through the
// checker's own element type — the [K, V] tuple for pair yields,
// T otherwise — built/read once into a hidden per-iteration
// local, the array for-of's exact desugar.
const elemT = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(decl!.name));
if (yieldsPair) {
const shape = elemT?.kind === "record" ? lowerer.shapes.get(elemT.shapeId) : null;
if (
elemT?.kind !== "record" || !shape?.tuple || shape.fields.length !== 2 ||
!typeEquals(shape.fields.find((f) => f.name === "0")!.type, keyT) ||
!typeEquals(shape.fields.find((f) => f.name === "1")!.type, secondT)
) {
lowerer.badType(decl!.name, lowerer.checker.getTypeAtLocation(decl!.name)); // defensive: the lib declares [K, V]
}
} else if (!elemT || !typeEquals(elemT, singleT)) {
lowerer.badType(decl!.name, lowerer.checker.getTypeAtLocation(decl!.name)); // defensive: the lib declares T
}
// Derive the yielded layout from the represented container. JS
// inference can leave the pattern at any even when the Map's
// native key/value types are known after specialization.
const elemT: IrType = yieldsPair
? { kind: "record", shapeId: lowerer.shapes.intern([{ name: "0", type: keyT }, { name: "1", type: secondT }], true) }
: singleT;
const elemInit: IrExpr = yieldsPair
? {
kind: "recordLit",
@@ -12,6 +12,7 @@ import { dirname } from "node:path";
import * as posix from "node:path/posix";
import { pathToFileURL } from "node:url";
import type { Lowerer } from "./lowerer.js";
import { captureContextArguments } from "./function-context.js";
import { OBJECT_CALLABLE_VALUES } from "./surfaces.js";
import { wasiGuestPath } from "../../wasi-paths.js";
import { BIGINT_T, BYTES_ELEMENT_NAME, BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, type IrBytesElem, type IrExpr, type IrFunction, type IrJsOp, type IrLocal, type IrRecordShape, type IrStmt, type IrType, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, type SrcLoc, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canDynCheckTo, canBoxFuncIntoDyn, funcOf, isDynTypedRefType, isSupportedArrayElem, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionContainerArmsOk } from "../../ir/ir.js";
@@ -30,11 +31,12 @@ import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUn
import { expandoMemberRead, expandoWritableTarget } from "./lower-expando.js";
import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.js";
import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget, storedClassValueType } from "./lower-classes.js";
import { hasRuntimeStatics } from "./class-runtime-statics.js";
import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
import { mixinFnOfCallee } from "./lower-mixins.js";
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion, withUnitArm } from "../type-mapper.js";
import { lowerYield } from "./lower-generators.js";
import { errorToStringCall } from "./error-methods.js";
import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./error-methods.js";
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
import { countedFor, numLit, varRef } from "../../ir/build.js";
import { unionWideningTags } from "../../ir/analysis.js";
@@ -54,7 +56,8 @@ 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 { builtinPrototypeMethod, lowerArrayIsArrayValue, lowerCheckedPredicateValue, lowerNumberParserValue, lowerObjectAssignValue, lowerStringCodesValue } from "./lower-builtin-values.js";
import { lowerArrayFromValue } from "./lower-containers.js";
import { lowerPerfHooksTypeof } from "./lower-builtins.js";
/** An assignable `obj.field` target — a class field, a record field, or a
@@ -769,8 +772,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// `arguments` in a variadic JS function (the rest-marked form): the
// synthetic trailing dyn-array param — lambdaSignature marked the
// type and lowerLambda declared the local.
if (expr.text === "arguments" && lowerer.ctx.argumentsLocal) {
return { kind: "varRef", localId: lowerer.ctx.argumentsLocal.id, type: DYN, loc };
if (expr.text === "arguments") {
const binding = captureContextArguments(lowerer.fnStack);
if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc };
}
// A compiler-projected adapter as a VALUE: the adapter has no
// runtime function object (its calls rewrite directly) — call it
@@ -2025,14 +2029,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// is an ordinary recordGet — maybeNarrow then bridges the union slot
// to the checker's narrowed arm, exactly like a union field read.
if (expr.name.text === "done" || expr.name.text === "value") {
const recvSym = lowerer.typeOf(expr.expression).getSymbol();
const receiverType = lowerer.typeOf(expr.expression);
const recvSym = receiverType.getAliasSymbol()?.name === "IteratorResult" ? receiverType.getAliasSymbol() : receiverType.getSymbol();
if (
(recvSym?.name === "IteratorYieldResult" || recvSym?.name === "IteratorReturnResult") &&
(recvSym?.name === "IteratorResult" || recvSym?.name === "IteratorYieldResult" || recvSym?.name === "IteratorReturnResult") &&
lowerer.checker.declarationsOf(recvSym).some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
(d) => (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && lowerer.isStdlibFile(d.getSourceFile()),
)
) {
const recv = lowerer.lowerExpr(expr.expression);
if (recv.type.kind === "dyn") return lowerer.maybeNarrow({ kind: "dynKeyGet", value: recv,
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }, expr);
if (recv.type.kind === "record") {
const shape = lowerer.shapes.get(recv.type.shapeId);
const field = shape?.fields.find((f) => f.name === expr.name.text);
@@ -2139,11 +2146,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// on undefined/null receivers unless an earlier `?.` guards the
// chain. Scalar-narrowed occurrences bridge via maybeNarrow's
// validated dynCheck as usual.
if (recvLowered.type.kind === "dyn") {
if (recvLowered.type.kind === "dyn" || isJsSourceFile(expr.getSourceFile()) &&
["f64", "bool", "bigint", "string", "symbol"].includes(recvLowered.type.kind)) {
const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
const opt = hasOptionalChainGuard(expr.expression);
return lowerer.maybeNarrow(
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: recvLowered, type: DYN, loc },
{ kind: "dynKeyGet", key, ...(opt ? { optional: true as const } : {}), value: lowerer.coerceToExpected(recvLowered, DYN), type: DYN, loc },
expr,
);
}
@@ -2205,6 +2213,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// reports a class. Its property bag must remain reachable, while
// an absent element takes the normal checked-value TypeError.
const nativeReceiver = lowerer.stripUndefinedArm(recvLowered.type);
if (ts.isPrivateIdentifier(expr.name) && recvLowered.type.kind === "object") {
const field = lowerer.classes.get(recvLowered.type.className)?.fields.get(expr.name.text);
if (field) return { kind: "fieldGet", obj: recvLowered, className: recvLowered.type.className,
field: expr.name.text, type: field, loc };
}
if (nativeReceiver.kind === "object" && nativeReceiver.className === "%Error") {
return lowerer.maybeNarrow({ kind: "dynKeyGet", value: lowerer.coerceToExpected(recvLowered, DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, type: DYN, loc }, expr);
}
if (isDynTypedRefType(nativeReceiver)) {
const info = lowerer.classes.get(nativeReceiver.className);
if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream) {
@@ -2553,7 +2570,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
thenRaw.type.kind === "func" && elseRaw.type.kind === "func" &&
!typeEquals(thenRaw.type, elseRaw.type) &&
lowerer.dynConvertible(thenRaw.type) && lowerer.dynConvertible(elseRaw.type);
const type = callableJoin ? DYN :
// JavaScript inference can discard an initially-undefined field from
// a conditional's type even after that field acquired other values.
// A checked arm must retain its actual representation at the join.
const checkedJsJoin = isJsSourceFile(expr.getSourceFile()) &&
(thenRaw.type.kind === "dyn" || elseRaw.type.kind === "dyn") &&
lowerer.dynConvertible(thenRaw.type) && lowerer.dynConvertible(elseRaw.type);
const type = callableJoin || checkedJsJoin ? DYN :
thenRaw.type.kind === "array" && typeEquals(thenRaw.type, elseRaw.type)
? thenRaw.type
: dynJoin
@@ -3196,6 +3219,10 @@ function lowerNullishPair(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcL
const fresh = rest.length === 1 ? lowerer.emptyCollectionFor(expr.right, rest[0]!) : null;
if (fresh) return { kind: "nullish", left, right: fresh, type: fresh.type, loc };
const type = lowerer.irTypeOf(expr);
if (type.kind === "dyn" && lowerer.dynConvertible(left.type)) {
return { kind: "nullish", left: lowerer.coerceToExpected(left, DYN),
right: lowerer.lowerExprExpecting(expr.right, DYN), type: DYN, loc };
}
if (typeEquals(type, left.type) || (rest.length === 1 && typeEquals(type, rest[0]!))) {
const right = lowerer.lowerExprExpecting(expr.right, type);
return { kind: "nullish", left, right, type, loc };
@@ -3287,6 +3314,7 @@ function nullishBranches(
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
const isAnd = op === ts.SyntaxKind.AmpersandAmpersandToken;
const left = lowerer.lowerCondition(e.left);
if (left.kind === "boolLit" && left.value !== isAnd) return left;
// The right operand evaluates only when the left already answered
// (true for &&, false for ||) — aliased-typeof narrows the left
// PROVES under that polarity hold while it lowers (`type ===
@@ -3301,6 +3329,7 @@ function nullishBranches(
lowerer.runtimeOptionalLocals.add(lowerer.runtimeOptionalRootOf(runtimeOptionalLocal));
}
});
if (left.kind === "boolLit") return right;
return {
kind: "logical",
op: isAnd ? "&&" : "||",
@@ -3569,11 +3598,42 @@ function lowerPromiseThenPresence(
* the receiver type AND the ambient-file provenance of the member are
* verified — the name alone proves nothing. */
export function lowerIntrinsicProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") {
const value = lowerer.lowerNativeCallableValue({
fn: "dyn.ownKeys", params: [DYN], result: DYN, valueParams: [{ mode: "required", type: DYN }],
}, "Reflect.ownKeys", locOf(expr))!;
return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) };
}
if (lowerer.chainBlocked(expr)) return null;
const builtinPrototype = builtinPrototypeMethod(lowerer, expr);
if (builtinPrototype) return { kind: "libCall", fn: "dyn.builtinMethod", args: [
{ kind: "strLit", value: builtinPrototype, type: STRING, loc: locOf(expr) },
{ kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
], type: DYN, loc: locOf(expr) };
if (ts.isPropertyAccessExpression(expr.expression) &&
lowerer.stdlibGlobalMember(expr.expression, "Array") === "prototype") {
return { kind: "dynKeyGet", value: { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr.expression) },
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, type: DYN, loc: locOf(expr) };
}
if (expr.name.text === "apply" && ts.isPropertyAccessExpression(expr.expression) &&
lowerer.stdlibGlobalMember(expr.expression, "Function") === "prototype")
return { kind: "libCall", fn: "dyn.functionApply", args: [], type: DYN, loc: locOf(expr) };
if (lowerer.stdlibGlobalMember(expr, "Array") === "prototype")
return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) };
if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype")
return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) };
if (lowerer.stdlibGlobalMember(expr, "Array") === "isArray") return lowerArrayIsArrayValue(lowerer, locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Array") === "from") return lowerArrayFromValue(lowerer, locOf(expr));
const stringMember = lowerer.stdlibGlobalMember(expr, "String");
if (stringMember === "fromCharCode" || stringMember === "fromCodePoint") return lowerStringCodesValue(lowerer, stringMember, locOf(expr));
const numberMember = lowerer.stdlibGlobalMember(expr, "Number");
if (numberMember === "parseInt" || numberMember === "parseFloat") return lowerNumberParserValue(lowerer, numberMember, locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer") return lowerCheckedPredicateValue(lowerer, "Buffer.isBuffer", "buffer", locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Date") === "now") return lowerer.lowerNativeCallableValue({
fn: "date.now", params: [], result: F64, valueParams: [],
}, "Date.now", locOf(expr));
if (stdlibGlobalNameOf(lowerer, expr.expression) === "Object") {
if (expr.name.text === "assign") return lowerObjectAssignValue(lowerer, locOf(expr));
const fn = own(OBJECT_CALLABLE_VALUES, expr.name.text);
if (fn) return lowerer.lowerNativeCallableValue(fn, `Object.${expr.name.text}`, locOf(expr));
}
@@ -3924,9 +3984,10 @@ function lowerPromiseThenPresence(
const expected = kind === "string"
? STRING
: lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
let receiver = expected && (expected.kind === "string" || expected.kind === "array")
? strictReceiver(expected)
: lowerer.lowerExpr(expr.expression);
let receiver = lowerer.lowerExpr(expr.expression);
if (receiver.type.kind !== "dyn" && expected && (expected.kind === "string" || expected.kind === "array")) {
receiver = lowerer.runtimeOptionalPropertyReceiver(expr.expression, receiver, expected, name) ?? receiver;
}
if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 && kind === "array") {
const present = lowerer.stripUndefinedArm(receiver.type);
const helper = present.kind === "array"
@@ -3994,6 +4055,14 @@ function lowerPromiseThenPresence(
// destination can use the ordinary tuple-to-array coercion afterward.
const tsType = underConstAssertion(expr) ? lowerer.typeOf(expr) : ctxType ?? lowerer.typeOf(expr);
let mapped = expected ?? lowerer.mapTypeOf(tsType);
// An inferred destructuring pattern supplies a contextual [any, ...]
// tuple. Its positions do not erase the literal's inferred element
// types; explicit destinations already arrive through expected.
if (expected === undefined && ctxType && lowerer.checker.isTupleType(ctxType) &&
lowerer.checker.getTypeArguments(ctxType as ts.TypeReference).some((type) => (type.flags & ts.TypeFlags.Any) !== 0)) {
const inferred = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (inferred?.kind === "array" || inferred?.kind === "record" && lowerer.shapes.get(inferred.shapeId)?.tuple) mapped = inferred;
}
// A JS literal whose OWN inferred type is never-tainted
// (neverTaintedJsType — the evolving `const gb = []`, the mixed
// command tuple `['pwd', []]`) carries no element information: route
@@ -4167,7 +4236,7 @@ function lowerPromiseThenPresence(
// `unknown[]` annotation or a collapsed `(string | object)[]` maps
// to DYN wholesale now (mapType's dyn-element array rule), so the
// literal IS the dyn array.
if (isJsSourceFile(expr.getSourceFile()) || mapped?.kind === "dyn") {
if (isJsSourceFile(expr.getSourceFile()) || mapped?.kind === "dyn" || lowerer.checkerAnyArray(expr)) {
if (expr.elements.some(ts.isSpreadElement)) {
// Native collection drains return vectors of retained dyn
// elements. Reuse the ordinary spread builder, then expose the
@@ -4589,7 +4658,10 @@ export function lowerOptionalNumber(
const target = symbolFieldTarget(lowerer, expr);
if (target) return lowerer.maybeNarrow(lowerer.fieldGetExpr(target, locOf(expr), expr), expr);
let receiver = lowerer.lowerExpr(expr.expression);
if (receiver.type.kind === "func") {
const classInfo = receiver.type.kind === "classval" ? lowerer.classes.get(receiver.type.className) : undefined;
if (receiver.type.kind === "func" ||
(receiver.type.kind === "object" && RUNTIME_ERROR_CLASSES.has(receiver.type.className)) || isDynTypedRefType(receiver.type) ||
(classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) || ((receiver.type.kind === "map" || receiver.type.kind === "set") && lowerer.dynConvertible(receiver.type))) {
const fnName = jsFuncNameOf(expr.expression);
receiver = { kind: "dynFrom", value: receiver, type: DYN, loc: locOf(expr),
...(fnName !== null ? { fnName } : {}) };
@@ -4640,7 +4712,7 @@ export function lowerOptionalNumber(
const target = classFieldTarget(lowerer, expr.expression, receiverIr, key);
if (target) return lowerer.maybeNarrow(lowerer.fieldGetExpr(target, locOf(expr), expr), expr);
}
if (isDynTypedRefType(receiverIr)) {
if (receiverIr.className === "%Error" || isDynTypedRefType(receiverIr)) {
const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN);
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
return lowerer.maybeNarrow({ kind: "dynKeyGet", value: receiver, key, type: DYN, loc: locOf(expr) }, expr);
@@ -4788,16 +4860,20 @@ export function lowerOptionalNumber(
// Dispatch from that stored shape, as the array fallback below does.
if (receiverIr === null && obj.type.kind === "record") {
const shape = lowerer.shapes.get(obj.type.shapeId);
const index = tupleLiteralIndex(expr.argumentExpression);
const field = shape?.tuple && index !== null ? shape.fields.find((field) => field.name === String(index)) : null;
if (field) return lowerer.maybeNarrow({ kind: "recordGet", obj, shapeId: obj.type.shapeId, field: field.name, type: field.type, loc: locOf(expr) }, expr);
if (shape && !shape.tuple) {
return lowerer.lowerRecordKeyRead(expr, obj.type.shapeId, shape);
}
}
if (obj.type.kind === "dyn" || isDynTypedRefType(obj.type)) {
if (obj.type.kind === "dyn" || obj.type.kind === "object" && obj.type.className === "%Error" || isDynTypedRefType(obj.type)) {
const rawKey = lowerer.lowerExpr(expr.argumentExpression);
// Number, bool, and DYN keys stringify (ToPropertyKey) — the
// dyn-keyed read `catchWarning[warning.name]` where the property
// chain itself lowered dyn.
const key = rawKey.type.kind === "dyn" || rawKey.type.kind === "symbol"
const key = rawKey.type.kind === "dyn" || rawKey.type.kind === "symbol" ||
rawKey.type.kind === "union" && lowerer.dynConvertible(rawKey.type)
? lowerer.coerceToExpected(rawKey, DYN)
: lowerRecordPropertyKey(lowerer, rawKey, expr.argumentExpression);
if (key.type.kind === "string" || key.type.kind === "dyn") {
@@ -5122,8 +5198,9 @@ export function lowerOptionalNumber(
// Number AND checked-dynamic keys ride the JS-exact formatter —
// property keys ARE strings (o[k] is o[String(k)] in JS), and a dyn
// key (agent.sockets[agent.getName(...)]) stringifies the same way.
dk = lowerRecordPropertyKey(lowerer, dk, keyNode);
if (dk.type.kind !== "string") {
if (dk.type.kind === "union" && lowerer.dynConvertible(dk.type)) dk = lowerer.coerceToExpected(dk, DYN);
if (dk.type.kind !== "symbol" && dk.type.kind !== "dyn") dk = lowerRecordPropertyKey(lowerer, dk, keyNode);
if (dk.type.kind !== "string" && dk.type.kind !== "symbol" && dk.type.kind !== "dyn") {
lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
}
const read: IrExpr = { kind: "dynKeyGet", key: dk, value: obj, type: DYN, loc };
@@ -5170,6 +5247,19 @@ export function lowerOptionalNumber(
litKey !== null
? ({ kind: "strLit", value: litKey, type: STRING, loc: locOf(keyNode) } satisfies IrExpr)
: lowerer.lowerExpr(keyNode);
// Optional string/number keys need ToPropertyKey, not a boxed copy of
// the receiver. Keep record reads on their native storage so nested
// references retain identity across index-signature reads.
if (key.type.kind === "union" &&
!lowerer.unions.get(key.type.unionId)?.arms.some((arm) => arm.kind === "symbol" || arm.kind === "dyn")) {
key = lowerRecordPropertyKey(lowerer, key, keyNode);
}
if ((key.type.kind === "symbol" || key.type.kind === "dyn" || key.type.kind === "union") &&
lowerer.dynConvertible(key.type) && lowerer.dynConvertible(obj.type)) {
const read: IrExpr = { kind: "dynKeyGet", value: lowerer.coerceToExpected(obj, DYN),
key: lowerer.coerceToExpected(key, DYN), type: DYN, loc };
return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
}
// Runtime NUMBER keys ride the JS-exact formatter (o[n] is o[String(n)]
// in JS — the number-keyed-signature access path).
key = lowerRecordPropertyKey(lowerer, key, keyNode);
@@ -5650,6 +5740,9 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
return lowerer.fieldSetStmt(field, value, locOf(expr), target);
}
if (hasClassPayload(lowerer, receiver.type)) {
if (receiver.type.kind === "object" && receiver.type.className === "%Error") {
return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(target.expression, receiver, DYN)), loc: locOf(expr) };
}
lowerer.unsupported("SC1090", target, "writing undeclared properties through structural views of class instances");
}
}
@@ -5860,7 +5953,7 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
// write, anything else falls through to the fences.
if (receiverIr?.kind === "dyn" || receiverIr?.kind === "object" || receiverIr === null) {
const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
if (obj !== null && (obj.type.kind === "dyn" || isDynTypedRefType(obj.type))) {
if (obj !== null && (obj.type.kind === "dyn" || obj.type.kind === "object" && obj.type.className === "%Error" || isDynTypedRefType(obj.type))) {
const loc = locOf(expr);
const litKey = recordKeyLiteralText(target.argumentExpression);
let key: IrExpr =
@@ -5912,6 +6005,9 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
lowerer.unsupported("SC1090", target.expression, "assignment through a possibly missing nested array receiver");
}
const index = lowerOptionalNumber(lowerer, lowerer.lowerExpr(target.argumentExpression), locOf(target.argumentExpression), target.argumentExpression);
if (index.type.kind === "dyn" && lowerer.dynConvertible(arr.type)) {
return { kind: "exprStmt", expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(target.expression, arr, DYN)), loc: locOf(expr) };
}
if (index.type.kind !== "f64") {
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
}
@@ -6441,6 +6537,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
(ts.isPropertyAccessExpression(expr.operand) || ts.isElementAccessExpression(expr.operand)) &&
!expr.operand.questionDotToken
) {
if (ts.isPropertyAccessExpression(expr.operand) && isJsSourceFile(expr.getSourceFile())) {
const receiver = lowerer.lowerExpr(expr.operand.expression);
if (receiver.type.kind === "dyn") {
const local = lowerer.declareHiddenLocal("%incrementReceiver", DYN);
const old = lowerer.declareHiddenLocal("%incrementOld", DYN);
const next = lowerer.declareHiddenLocal("%incrementNext", DYN);
const value = varRef(local.id, DYN, loc);
const key: IrExpr = { kind: "strLit", value: expr.operand.name.text, type: STRING, loc };
return { kind: "seqExpr", stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{ kind: "varDecl", localId: old.id, init: { kind: "libCall", fn: "dyn.toNumeric", args: [
{ kind: "dynKeyGet", value, key, type: DYN, loc },
], type: DYN, loc }, loc },
{ kind: "varDecl", localId: next.id, init: { kind: "libCall", fn: "dyn.increment", args: [varRef(old.id, DYN, loc),
{ kind: "boolLit", value: op === "+", type: BOOL, loc }], type: DYN, loc }, loc },
{ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [value, key, varRef(next.id, DYN, loc)], type: VOID, loc }, loc },
], result: varRef(prefix ? next.id : old.id, DYN, loc), type: DYN, loc };
}
}
const target = ts.isPropertyAccessExpression(expr.operand)
? lowerer.fieldTarget(expr.operand)
: symbolFieldTarget(lowerer, expr.operand);
@@ -6583,14 +6698,14 @@ export function lowerCompoundValueToTarget(
}
if (
(target.type.kind === "dyn" || rhs.type.kind === "dyn") &&
(target.type.kind === "dyn" || target.type.kind === "f64") &&
(rhs.type.kind === "dyn" || rhs.type.kind === "f64") &&
isJsSourceFile(expr.getSourceFile())
) {
const checkNum = (value: IrExpr): IrExpr =>
value.type.kind === "dyn" ? { kind: "dynCheck", value, type: F64, loc: value.loc } : value;
const computed: IrExpr = { kind: "bin", op: compound, left: checkNum(read), right: checkNum(rhs), type: F64, loc };
return target.type.kind === "dyn" ? { kind: "dynFrom", value: computed, type: DYN, loc } : computed;
const left = lowerer.coerceInto(expr.left, read, DYN);
const right = lowerer.coerceInto(expr.right, rhs, DYN);
const numeric = ["-", "*", "/", "%", "**"].includes(compound);
const computed: IrExpr = { kind: "libCall", fn: compound === "+" ? "dyn.add" : numeric ? "dyn.arithmetic" : "dyn.bitwise",
args: compound === "+" ? [left, right] : [left, right, { kind: "strLit", value: compound, type: STRING, loc }], type: DYN, loc };
return lowerer.coerceInto(expr, computed, target.type);
}
lowerer.unsupported("SC1043", expr);
}
@@ -6723,8 +6838,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
// key afterward, then yields the RHS without rereading the property.
if (ts.isElementAccessExpression(expr.left) && !expr.left.questionDotToken) {
const recv = tryLowerExpression(lowerer, expr.left.expression);
if (recv?.type.kind === "dyn") {
return lowerDynMemberAssignment(lowerer, expr, recv);
if (recv && (recv.type.kind === "dyn" ||
recv.type.kind === "array" && isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(recv.type))) {
return lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(expr.left.expression, recv, DYN));
}
if (recv?.type.kind === "array" || recv?.type.kind === "bytes") {
const receiver = lowerer.declareHiddenLocal("%setArray", recv.type);
@@ -6949,8 +7065,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
const scalarSide = dynSide === left ? right : left;
const reference = scalarSide.type;
if ((isDynTypedRefType(reference) || reference.kind === "record" || reference.kind === "array" ||
reference.kind === "bytes" || reference.kind === "func" || reference.kind === "regex" || reference.kind === "url" ||
reference.kind === "bigint" || reference.kind === "symbol" || reference.kind === "set" || reference.kind === "union" || DYN_HANDLE_KINDS.has(reference.kind)) &&
reference.kind === "bytes" || reference.kind === "func" || reference.kind === "classval" || reference.kind === "regex" || reference.kind === "url" ||
reference.kind === "bigint" || reference.kind === "symbol" || reference.kind === "map" || reference.kind === "set" || reference.kind === "union" || DYN_HANDLE_KINDS.has(reference.kind)) &&
lowerer.dynConvertible(reference)) {
const boxed: IrExpr = {
kind: "dynFrom", value: scalarSide, type: DYN, loc: scalarSide.loc,
@@ -6999,7 +7115,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
}
// Numeric operators dispatch on ToNumeric's actual Number/BigInt
// result, including values flowing through native callable properties.
if (isJsSourceFile(expr.getSourceFile()) || op === ts.SyntaxKind.PlusToken) {
if (isJsSourceFile(expr.getSourceFile()) || op === ts.SyntaxKind.PlusToken ||
[ts.SyntaxKind.MinusToken, ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken,
ts.SyntaxKind.PercentToken, ts.SyntaxKind.AsteriskAsteriskToken].includes(op)) {
const checkNum = (e: IrExpr): IrExpr =>
e.type.kind === "dyn" ? { kind: "dynCheck", value: e, type: F64, loc: e.loc } : e;
const other = left.type.kind === "dyn" ? right : left;
@@ -7524,6 +7642,14 @@ function lowerLogicalChain(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrExpr): IrExpr {
const op = expr.operatorToken.kind;
const loc = locOf(expr);
// Preserve short-circuiting during lowering too: an unreachable member
// on a specialized value must not poison the surrounding JS statement.
if (left.kind === "boolLit") {
const takeRight = op === ts.SyntaxKind.AmpersandAmpersandToken ? left.value : !left.value;
return takeRight ? lowerer.lowerExpr(expr.right) : left;
}
if (left.kind === "unitLit" && op === ts.SyntaxKind.AmpersandAmpersandToken) return left;
if (op === ts.SyntaxKind.BarBarToken && REF_TRUTHY_KINDS.has(left.type.kind)) return left;
const right = lowerer.lowerExpr(expr.right);
// A LITERAL-unit left operand (a compile-time undefined/null — the
// capability-probe members: `process.features.inspector ||
@@ -8551,6 +8677,10 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
);
}
const left = lowerer.lowerExpr(expr.left);
if (left.type.kind === "dyn") {
const classValue = lowerer.lowerExpr(expr.right);
return classInstanceOf(lowerer, left, targetInfo, loc, classValue);
}
if (left.type.kind !== "object") {
lowerer.unsupported(
"SC1090",
@@ -8940,9 +9070,9 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
// `in` shares the coercion).
{
const probed = tryLowerExpression(lowerer, expr.right);
if (probed && (probed.type.kind === "dyn" || isDynTypedRefType(probed.type))) {
if (probed && lowerer.dynConvertible(probed.type)) {
const k = lowerer.lowerExpr(expr.left); // JS order: the key evaluates first
if (k.type.kind === "symbol" || k.type.kind === "dyn" || k.type.kind === "string" || k.type.kind === "f64") {
if (lowerer.dynConvertible(k.type)) {
const boxed = lowerer.coerceToExpected(k, DYN);
const keyLocal = lowerer.declareHiddenLocal("%inKey", DYN);
return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: keyLocal.id, init: boxed, loc }],
@@ -8976,7 +9106,9 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
return { kind: "unionIsTag", unionId: envType.unionId, tag: undefTag, negated: true, value: read, type: BOOL, loc };
}
let recv = lowerer.lowerExpr(expr.right);
if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "func" && lowerer.dynConvertible(recv.type)) {
const classInfo = recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
if ((classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) ||
(isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "func" && lowerer.dynConvertible(recv.type))) {
recv = lowerer.coerceToExpected(recv, DYN);
}
const siteType = lowerer.mapTypeOf(lowerer.typeOf(expr.right));
@@ -9900,7 +10032,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
// instance argument (e.g. Matrix4.makeTranslation). Preserve primitive
// missing-property semantics and dispatch class data through its native
// capsule instead of demanding a field shared by every union arm.
if (isJsSourceFile(expr.getSourceFile()) && def.arms.some(isDynTypedRefType) && lowerer.dynConvertible(value.type)) {
if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type)) {
return { kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), key,
...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}), type: DYN, loc: locOf(expr) };
}
@@ -10120,7 +10252,7 @@ function representedClassFieldTarget(
}
const stored = ts.isIdentifier(access.expression)
? lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type : undefined;
const receiverIr = stored?.kind === "record" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
const receiverIr = stored?.kind === "record" || stored?.kind === "object" ? stored : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (receiverIr?.kind === "object") {
return classFieldTarget(lowerer, access.expression, receiverIr, access.name.text);
}
@@ -10305,6 +10437,8 @@ function representedClassFieldTarget(
* tsc-clean (the property types as the setter's param), but Node yields
* undefined, which these property types cannot represent. */
export function fieldGetExpr(lowerer: Lowerer, target: FieldTarget, loc: SrcLoc, blame: ts.Node): IrExpr {
if (target.container === "class" && (target.field === "message" || target.field === "name") && (target.className === "%Error" || lowerer.isSubclassOf(target.className, "%Error")))
return errorPropertyRead(lowerer, target.obj, target.field);
if (target.container === "errorStackLimit") {
return { kind: "libCall", fn: "error.stackLimitGet", args: [], type: F64, loc };
}
@@ -10422,6 +10556,8 @@ function representedClassFieldTarget(
* call). A write to a getter-only property never gets here in a clean
* program (tsc's TS2540 is the fence); the rejection is the backstop. */
export function fieldSetStmt(lowerer: Lowerer, target: FieldTarget, value: IrExpr, loc: SrcLoc, blame: ts.Node): IrStmt {
if (target.container === "class" && (target.field === "message" || target.field === "name") && (target.className === "%Error" || lowerer.isSubclassOf(target.className, "%Error")))
return errorPropertyWrite(lowerer, target.obj, value, target.field);
if (target.container === "errorStackLimit") {
return { kind: "exprStmt", expr: { kind: "libCall", fn: "error.stackLimitSet", args: [value], type: VOID, loc }, loc };
}
@@ -10538,14 +10674,14 @@ function representedClassFieldTarget(
// identity survives to catch/unhandledRejection observers), and the
// result is promise<dyn> when no context names a concrete one.
const reason = lowerer.lowerExpr(reasonNode);
if (reason.type.kind === "dyn") {
if (reason.type.kind === "dyn" || lowerer.dynConvertible(reason.type) && reason.type.kind !== "object") {
// The result only ever REJECTS, so the inner type is unobservable:
// adopt the context's promise type when one names it, else the
// checker's own Promise<never> reading (promise<void> — exactly the
// Error-reason form below).
const resultT: IrType =
type?.kind === "promise" ? type : { kind: "promise", inner: VOID };
return { kind: "intrinsic", name: "promise.reject", args: [reason], type: resultT, loc };
return { kind: "intrinsic", name: "promise.reject", args: [lowerer.coerceInto(reasonNode, reason, DYN)], type: resultT, loc };
}
// An Error reason with NO context-named promise type (a bare
// top-level `Promise.reject(new Error())` — the unhandled-rejection
@@ -245,6 +245,17 @@ function generatorLoopBinding(
const list = stmt.initializer;
const decl = list.declarations[0]!;
const isLet = (list.flags & ts.NodeFlags.Let) !== 0;
// The iterator result uses checked storage when its return channel is
// unknown. After the done test, restore the declared yield shape for
// destructuring without exposing the unrelated return value.
if (valueType.kind === "dyn" && (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name))) {
const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
if (declared?.kind === "record" || declared?.kind === "array") {
const converted = lowerer.coerceToExpected(value, declared);
value = converted;
valueType = converted.type;
}
}
if (ts.isIdentifier(decl.name)) {
const varTarget = forOfVarTarget(lowerer, decl);
const bound = varTarget
@@ -557,7 +568,7 @@ function lowerYieldStar(lowerer: Lowerer, expr: ts.YieldExpression): IrExpr {
if (!expr.expression) lowerer.unsupported("SC1071", expr, "'yield*' with no operand");
const loc = locOf(expr);
const delegate = lowerer.lowerExpr(expr.expression);
if (delegate.type.kind === "dyn" || delegate.type.kind === "object") return lowerCheckedDelegation(lowerer, expr, delegate);
if (delegate.type.kind === "dyn" || delegate.type.kind === "object" || delegate.type.kind === "classval") return lowerCheckedDelegation(lowerer, expr, delegate);
if (delegate.type.kind !== "generator") {
lowerer.unsupported(
"SC1071",
@@ -3454,6 +3454,10 @@ export function lowerStaticReadableStreamReaderCall(
return { kind: "numLit", value: staticValue, type: F64, loc };
}
const native = own(STATIC_MATH_FNS, member);
if (native && (member === "min" || member === "max" || member === "hypot")) return lowerer.lowerNativeCallableValue({
fn: member === "min" ? "math.minArr" : member === "max" ? "math.maxArr" : "math.hypotArr",
params: [arrayOf(F64)], result: F64, valueParams: [{ mode: "rest", type: F64 }],
}, `Math.${member}`, loc);
if (native && member !== "min" && member !== "max" && member !== "hypot") {
const params = Array.from({ length: native.arity }, () => F64);
return lowerer.lowerNativeCallableValue({
@@ -28,7 +28,7 @@ import { collectExpandoMembers } from "./lower-expando.js";
import { recordTextCodecClass } from "../../ir/ir.js";
import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
import type { ClassInfo } from "./lower-classes.js";
import { collectVirtualJsMethods, decoratorNodesOf, genericIfaceBindingKeepsClass, guaranteedDecorationThrow } from "./lower-classes.js";
import { collectVirtualJsMethods, decoratorNodesOf, genericIfaceBindingKeepsClass, guaranteedDecorationThrow, storedClassValueType } from "./lower-classes.js";
import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
import { cjsModuleRef, cjsModuleRegistryPrelude } from "./lower-node-module.js";
import { forkTargetPaths } from "../fork-target.js";
@@ -250,10 +250,32 @@ export function appendForkModules(
* signatures are cheap and calls resolve against them; only BODY lowering
* is reachability-gated. */
export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
const computed = new Set<ts.ClassDeclaration>();
for (const fp of parts) for (const decl of fp.classDecls) {
const heritage = decl.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression;
if (heritage && ts.isCallExpression(heritage)) computed.add(decl);
}
// Descendants need the computed ancestor's completed layout before
// their own fields and inherited methods can be collected.
const computedSymbols = new Set([...computed].map((decl) => declSymbolOf(lowerer, decl)).filter((symbol) => symbol !== undefined));
let expanded = true;
while (expanded) {
expanded = false;
for (const fp of parts) for (const decl of fp.classDecls) {
if (computed.has(decl)) continue;
const heritage = decl.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types[0]?.expression;
const base = heritage && ts.isIdentifier(heritage) ? lowerer.resolveValueSymbol(heritage) : undefined;
if (!base || !computedSymbols.has(base)) continue;
computed.add(decl);
const symbol = declSymbolOf(lowerer, decl);
if (symbol) computedSymbols.add(symbol);
expanded = true;
}
}
lowerer.collecting = true;
try {
collectVirtualJsMethods(lowerer, parts.map((part) => part.sf));
for (const fp of parts) for (const decl of fp.classDecls) lowerer.collectClassShape(decl);
for (const fp of parts) for (const decl of fp.classDecls) if (!computed.has(decl)) lowerer.collectClassShape(decl);
for (const fp of parts) for (const decl of fp.fnDecls) lowerer.collectSignature(decl);
} finally {
lowerer.collecting = false;
@@ -276,6 +298,52 @@ export function appendForkModules(
lowerer.collectNpmImports(parts);
lowerer.collectJsonImports(parts);
}
for (const decl of computed) {
const symbol = declSymbolOf(lowerer, decl);
const wasCollecting: boolean = lowerer.collecting;
lowerer.collecting = true;
const previousSink = lowerer.diagSink;
const deferred: import("../../diagnostics/diagnostic.js").ScrDiagnostic[] = [];
lowerer.diagSink = deferred;
const expression = decl.heritageClauses!.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)!.types[0]!.expression;
const context = newFnCtx(false, null, null, DYN);
lowerer.fnStack.push(context);
try {
let value = lowerer.lowerExpr(expression);
const known = value.type.kind === "dyn" ? storedClassValueType(lowerer, expression) : null;
if (known?.kind === "classval") value = lowerer.coerceToExpected(value, known);
if (value.type.kind === "dyn" || value.type.kind === "func") {
const name = `%class.base.${lowerer.classNamer(decl)}`;
const loc = locOf(expression);
lowerer.liftedFns.push({ name, params: [], returnType: value.type, locals: context.locals,
body: [{ kind: "return", value, loc }], loc });
lowerer.computedCallableBases.set(decl, { kind: "call", callee: name, args: [], type: value.type, loc });
}
if (value.type.kind === "classval") {
const classInfo = lowerer.classes.get(value.type.className);
if (classInfo) {
const name = `%class.base.${lowerer.classNamer(decl)}`;
const loc = locOf(expression);
lowerer.liftedFns.push({ name, params: [], returnType: value.type, locals: context.locals,
body: [{ kind: "return", value, loc }], loc });
lowerer.computedClassBases.set(lowerer.classNamer(decl), { value: { kind: "call", callee: name, args: [], type: value.type, loc }, classInfo });
}
}
} catch (error) {
if (!(error instanceof PoisonError)) throw error;
} finally {
lowerer.fnStack.pop();
lowerer.diagSink = previousSink;
}
lowerer.diagSink = deferred;
try { lowerer.collectClassShape(decl); }
finally { lowerer.diagSink = previousSink; }
if (symbol && deferred.length) {
lowerer.deferredDiags.set(symbol, [...(lowerer.deferredDiags.get(symbol) ?? []), ...deferred]);
lowerer.deferredClassByName.set(lowerer.classNamer(decl), symbol);
}
lowerer.collecting = wasCollecting;
}
}
/** The npm-import chokepoint, run once per pass during collection (so
@@ -814,7 +882,7 @@ function cjsScalarLiteral(e: ts.Expression): boolean {
* a checked-dynamic (or unit) value, so a DYN module global can hold it:
* no initializer, a unit literal, or a checker-`any` call/member/
* identifier read (any-typed operations lower through the checked-dynamic tree in JS).
* `new` expressions stay OUT even when checker-any (`new Anon.Sub()` over
* User-class `new` expressions stay OUT even when checker-any (`new Anon.Sub()` over
* an expando class member constructs a TYPED instance — corpus 2032), as
* does everything else (object/array literals have their own rules;
* typed-but-unmappable initializers keep the %init-local adoption). */
@@ -833,12 +901,17 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
let e: ts.Expression = init;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (checkedGlobalReference(lowerer, e)) return true;
// The built-in Array constructor can create an evolving array whose
// inferred element type has no native mapping. Its checked reference
// must remain shared with separately declared functions and aliases.
if ((ts.isCallExpression(e) || ts.isNewExpression(e)) && lowerer.isStdlibGlobal(e.expression, "Array")) return true;
// JavaScript process streams lower to native checked handles even when
// their declared method overloads prevent a structural type mapping.
// Register the shared slot before separately declared functions lower.
if (ts.isPropertyAccessExpression(e)) {
const member = lowerer.stdlibGlobalMember(e, "process");
if (member === "stdin" || member === "stdout" || member === "stderr") return true;
if (e.name.text === "constructor" && lowerer.checker.getCallSignatures(lowerer.typeOf(e.expression)).length > 0) return true;
}
if (e.kind === ts.SyntaxKind.NullKeyword) return true;
if (ts.isIdentifier(e) && e.text === "undefined") return true;
@@ -1111,8 +1184,11 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
const diagsBefore = lowerer.diags.length;
try {
const strict = lowerer.typeOf(rhs);
const t = lowerer.mapTypeOf(strict) ??
let t = lowerer.mapTypeOf(strict) ??
(fnValued || factoryValued ? dynFallbackType(lowerer, rhs, strict) : null);
// A returned object remains the live CommonJS root; later
// exports.member writes must update that same open object.
if (factoryValued && t?.kind === "record" && lowerer.dynConvertible(t)) t = DYN;
if (t && t.kind !== "void") {
const g: IrGlobal = { id: `%g.${tag}exports`, name: "exports", type: t, mutable: false };
lowerer.globalsByDeclNode.set(cjs.expr, g);
@@ -1659,7 +1735,20 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
// exactly the local rule (uncheckedOverloadHandleCall).
(uncheckedOverloadHandleCall(lowerer, decl.initializer) ? JSVAL : null) : null)
: null;
let type = handleT ?? factoryType ?? lowerer.irTypeOf(nameNode);
// Unannotated results of JavaScript calls can carry callable
// objects or heterogeneous unions absent from the native type
// map. Their compiled initializer still validates every crossing.
const callSignature = !decl.type && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isCallExpression(decl.initializer)
? lowerer.checker.getResolvedSignature(decl.initializer) : undefined;
const callDeclaration = callSignature && lowerer.checker.signatureDeclaration(callSignature);
const inferredJsCall = callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) &&
!lowerer.mapTypeOf(lowerer.typeOf(nameNode));
let type = handleT ?? factoryType ?? (inferredJsCall ? DYN : lowerer.irTypeOf(nameNode));
if (isJsSourceFile(sf) && !decl.type && !hasJsTypeAnnotation(decl) &&
decl.initializer && ts.isCallExpression(decl.initializer) && type.kind === "union") {
const arms = lowerer.unions.get(type.unionId)?.arms;
if (arms?.some((arm) => arm.kind === "record") && arms.every((arm) => isUnitType(arm) || arm.kind === "record")) type = DYN;
}
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;
@@ -1783,6 +1872,9 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN;
}
// Module vars hold real undefined until their declaration runs.
// Unannotated JS function values need that state too: callers
// before the initializer must see undefined and a TypeError.
if (isJsSourceFile(sf) && isVarDeclared(decl) && type.kind === "func" && lowerer.dynConvertible(type)) type = DYN;
// Codec records keep that state in the ordinary optional union;
// lexical codec declarations retain their separate TDZ guard.
const codec = type.kind === "record" && recordTextCodecClass(lowerer.shapes.get(type.shapeId)!) !== null;
@@ -2188,7 +2280,7 @@ export function collectGlobals(lowerer: Lowerer, sf: ts.SourceFile, topStmts: ts
// SITE's position instead (below) — their inner class node may
// even live in another file.
c.decl && ts.isClassDeclaration(c.decl) && !c.mixinInstance && c.decl.getSourceFile() === sf &&
(c.callableBase !== undefined || c.staticFields.length > 0 || (c.staticBlocks?.length ?? 0) > 0 || c.classDecorators !== undefined))
(c.callableBase !== undefined || c.def.baseValueGlobal !== undefined || c.staticFields.length > 0 || (c.staticBlocks?.length ?? 0) > 0 || c.classDecorators !== undefined))
.map((c) => ({ pos: c.decl!.getStart(), info: c }));
// Statics-bearing MIXIN instantiations whose call evaluates in THIS
// file: their declaration-time code runs when the call does — the
@@ -1606,8 +1606,10 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
dynSpell?: string,
allowDynObject = false,): void {
if (ts.isObjectBindingPattern(pattern)) {
const patternSymbol = lowerer.typeOf(pattern).getSymbol();
const patternType = lowerer.typeOf(pattern);
const patternSymbol = patternType.getSymbol();
allowDynObject ||= patternSymbol?.name === "SegmentData" && lowerer.isStdlibSymbol(patternSymbol);
allowDynObject ||= (patternType.flags & ts.TypeFlags.Object) !== 0 && lowerer.mapTypeOf(patternType)?.kind === "dyn";
}
const present = patternSourceValue(lowerer, srcRef(), ts.isArrayBindingPattern(pattern), out, locOf(pattern), dynSpell);
if (!typeEquals(present.type, srcType)) {
@@ -1769,20 +1771,22 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
out.push({
kind: "varDecl",
localId: pack.id,
init: {
init: pattern.elements.some((element) => ts.isBindingElement(element) && element.dotDotDotToken) ? {
kind: "libCall",
fn: "dyn.iterPack",
args: [srcRef(), { kind: "strLit", value: dynSpell ?? "", type: STRING, loc }],
type: DYN,
loc,
},
} : { kind: "dynIterN", value: srcRef(), count: pattern.elements.length, ...(dynSpell ? { notIterableMessage: dynSpell } : {}), type: DYN, loc },
loc,
});
pattern.elements.forEach((el, i) => {
if (ts.isOmittedExpression(el) || el.name === undefined) return; // hole: the position skips
const elLoc = locOf(el);
if (el.dotDotDotToken) {
lowerer.unsupported("SC1031", el, "rest elements over checked-dynamic sources");
lowerer.bindPatternTarget(el.name, { kind: "dynInvoke", recv: varRef(pack.id, DYN, elLoc),
method: "slice", calleeName: "Array.prototype.slice", args: [lowerer.coerceToExpected(numLit(i, elLoc), DYN)], type: DYN, loc: elLoc }, isLet, out);
return;
}
let value: IrExpr = {
kind: "dynKeyGet",
@@ -1796,7 +1800,7 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
// its value converting into the checked-dynamic tree like any dyn-slot value.
const rl = lowerer.declareHiddenLocal("%delem", DYN);
out.push({ kind: "varDecl", localId: rl.id, init: value, loc: elLoc });
const dflt = lowerer.coerceToExpected(lowerer.lowerExpr(el.initializer), DYN);
const dflt = lowerer.coerceInto(el.initializer, lowerer.lowerExpr(el.initializer), DYN);
if (dflt.type.kind !== "dyn") {
lowerer.unsupported(
"SC1031",
@@ -3779,7 +3783,9 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
// poisons, still declare the local when its type is representable, so
// later references to this name don't produce cascading errors.
const inferredObjectType = isJsSourceFile(decl.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(decl.name)) : null;
const expandsObject = inferredObjectType?.kind === "record" && jsObjectBindingExpands(lowerer, decl, inferredObjectType);
const inferredObjectArms = inferredObjectType?.kind === "union"
? lowerer.unions.get(inferredObjectType.unionId)?.arms ?? [] : inferredObjectType ? [inferredObjectType] : [];
const expandsObject = inferredObjectArms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm));
let init: IrExpr;
try {
init = expandsObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type &&
@@ -3845,10 +3851,12 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
}
const preservesObjectIdentity = init.type.kind === "dyn" &&
(inferredObjectType?.kind === "record" || inferredObjectType?.kind === "array" ||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectArms.some((arm) => arm.kind === "record") &&
inferredObjectArms.every((arm) => isUnitType(arm) || arm.kind === "record")) ||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
(inferredObjectType.kind === "date" || inferredObjectType.kind === "func" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) ||
(inferredObjectType.kind === "date" || inferredObjectType.kind === "func" || inferredObjectType.kind === "object" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) ||
(isJsSourceFile(decl.getSourceFile()) && inferredObjectType !== null &&
(isUnitType(inferredObjectType) || inferredObjectType.kind === "union" && lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType)))) &&
(inferredObjectType.kind === "void" || isUnitType(inferredObjectType) || inferredObjectType.kind === "union" && lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType)))) &&
!decl.type && !hasJsTypeAnnotation(decl);
if (expandsObject || preservesObjectIdentity || (isJsSourceFile(decl.getSourceFile()) &&
!decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer))) {
@@ -4167,7 +4175,7 @@ export function lowerVarDecl(lowerer: Lowerer, decl: ts.VariableDeclaration, isL
if (decl.type || !decl.initializer || !ts.isIdentifier(decl.name) || !isJsSourceFile(decl.getSourceFile())) return false;
let initializer = decl.initializer;
while (ts.isParenthesizedExpression(initializer)) initializer = initializer.expression;
if (!ts.isObjectLiteralExpression(initializer)) return false;
if (!ts.isObjectLiteralExpression(initializer) && !ts.isCallExpression(initializer)) return false;
if (hasJsTypeAnnotation(decl)) return false;
// A generic clone retains its declared T contract. Its instantiated
// record is also the factory's return/capture layout; it is not an
@@ -4410,9 +4418,10 @@ function lowerBranchSwitch(
* narrowing tests (`e instanceof C` over hierarchy classes, `typeof e
* === "string"/"number"/"boolean"`), reads under a proven narrow
* (caughtRead bridges them with caughtNarrow, trust-the-checker like
* unionNarrow), and rethrow (`throw e`). Raw uses, captures, and
* assignment are fenced (SC1063 and friends); destructuring patterns
* are SC1062. The `: any` / `: unknown` annotations tsc admits both
* unionNarrow), and rethrow (`throw e`). Unsupported raw uses and
* assignments are fenced (SC1063 and friends); destructuring patterns
* are SC1062. Captures retain the snapshot beyond the catch scope.
* The `: any` / `: unknown` annotations tsc admits both
* lower the same way (tsc narrows either through the tests).
* - `finally` runs on normal completion, exception paths, and every
* abrupt completion that crosses it. Return values are snapshotted
@@ -5358,6 +5367,13 @@ function isStrictDelete(node: ts.DeleteExpression): boolean {
}
if (!expr.left.questionDotToken) {
const recv = tryLowerExpression(lowerer, expr.left.expression);
if (recv?.type.kind === "object" && recv.type.className === "%Error") {
const loc = locOf(expr);
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [
lowerer.coerceToExpected(recv, DYN), { kind: "strLit", value: expr.left.name.text, type: STRING, loc },
lowerer.lowerExprExpecting(expr.right, DYN),
], type: VOID, loc }, loc };
}
if (recv && isDynTypedRefType(recv.type)) {
const info = lowerer.classes.get(recv.type.className);
if (info && !info.def.runtime && !info.builtinError && !info.builtinEmitter && !info.builtinStream &&
@@ -1016,6 +1016,9 @@ export function jsFuncNameOf(node: ts.Node): string | null {
if (p && ts.isVariableDeclaration(p) && p.initializer === n && ts.isIdentifier(p.name)) {
return p.name.text;
}
if (p && ts.isBindingElement(p) && p.initializer === n && ts.isIdentifier(p.name)) {
return p.name.text;
}
if (p && ts.isPropertyAssignment(p) && p.initializer === n && ts.isIdentifier(p.name)) {
return p.name.text;
}
@@ -1831,7 +1834,10 @@ export class Lowerer {
/** One Error-method helper belongs to this lowering session. Its cached
* dispatch revision needs no weak-key identity or process-global state. */
errorMethodDispatchRevision: string | null = null;
errorPropertyDispatchRevision: string | null = null;
lambdaCounter = 0;
readonly preparedClassFactories = new Map<ts.ArrowFunction | ts.FunctionExpression, IrExpr>();
readonly preparingClassFactories = new Set<ts.ArrowFunction | ts.FunctionExpression>();
/** Statement lists currently mid-lowering, innermost last: the forward-
* capture machinery needs to know which later statements of an OPEN list
@@ -1977,6 +1983,22 @@ export class Lowerer {
? { kind: "object", className: this.mixinTypeContext.className }
: null,
localClassInstance: (decl) => {
// Computed heritage can close over parameters of its factory.
// A speculative checker query outside that factory must not lower
// the extends expression in the caller's lexical scope.
if (!this.classes.has(this.classNamer(decl)) && !this.collectingExprClasses.has(decl) &&
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.some((base) => {
const expression = base.expression;
if (ts.isCallExpression(expression)) return true;
if (ts.isIdentifier(expression) && this.isStdlibGlobal(expression, expression.text)) return false;
const symbol = this.checker.getSymbolAtLocation(ts.isPropertyAccessExpression(expression) ? expression.name : expression);
const resolved = symbol && symbol.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(symbol) : symbol;
if (ts.isPropertyAccessExpression(expression) && this.isStdlibGlobal(expression.expression, "globalThis") &&
this.builtinErrorInfoOf(resolved)) return false;
if (resolved && this.classBySymbol.has(resolved)) return false;
const mapped = this.mapTypeOf(this.typeOf(expression));
return mapped?.kind !== "classval" || !this.classes.has(mapped.className);
}))) return null;
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
if (!ts.isFunctionDeclaration(parent) && !ts.isFunctionExpression(parent) &&
!ts.isArrowFunction(parent) && !ts.isMethodDeclaration(parent)) continue;
@@ -2516,7 +2538,7 @@ export class Lowerer {
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
const directSpec = requireSpecOf(decl.initializer);
if (directSpec !== null) {
return resolveImport(this.program, decl.getSourceFile(), directSpec) ??
return resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ??
npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require");
}
return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null;
@@ -2816,7 +2838,7 @@ export class Lowerer {
// Without the guarded %init call at this position
// those globals stay uninitialized: the dep's module body would never
// run.
const dep = resolveImport(this.program, node.getSourceFile(), spec) ??
const dep = resolveImport(this.program, node.getSourceFile(), spec, "require") ??
npmStaticDepSf7(this.program, node.getSourceFile(), spec, "require");
if (!dep || dep.fileName.endsWith(".json")) return null;
if (this.asyncInitFiles.has(dep)) {
@@ -3982,6 +4004,8 @@ export class Lowerer {
// before collectProgram visits them one by one; reached body lowering
// will reuse the same answers later.
this.prefetchClassCollection(parts.flatMap((fp) => fp.classDecls));
const collectionEdges = new Set<string>();
this.onEdge = (name) => { collectionEdges.add(name); };
this.collectProgram(parts);
this.analyzeRuntimeOptionalArrayReads(parts);
// Decorated classes analyze post-collection here too: the %init seeds
@@ -4050,13 +4074,13 @@ export class Lowerer {
units.set(`%${cName}.constructor`, {
order: unitOrder++,
roots: classCtorRoots(info),
lower: () => this.lowerClassCtor(info),
lower: () => info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info),
});
for (const { mName, member } of this.classMethodMembers(info)) {
units.set(`%${cName}.${mName}`, {
order: unitOrder++,
roots: functionRoots(member),
lower: () => this.lowerClassMethodMember(info, member),
lower: () => info.localClass && !info.localClass.ready ? null : this.lowerClassMethodMember(info, member),
});
}
for (const prop of info.throwingSetters) {
@@ -4146,14 +4170,15 @@ export class Lowerer {
lower: () => IrFunction | null,
): void => {
units.set(name, { order: unitOrder++, roots, lower });
if (reachable.has(name)) queue.push(name);
metadataPriority.set(name, [3, expressionMetadataOrder++]);
};
register(`%${cName}.constructor`, classCtorRoots(info), () => this.lowerClassCtor(info));
register(`%${cName}.constructor`, classCtorRoots(info), () => info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info));
for (const { mName, member } of this.classMethodMembers(info)) {
register(
`%${cName}.${mName}`,
functionRoots(member),
() => this.lowerClassMethodMember(info, member),
() => info.localClass && !info.localClass.ready ? null : this.lowerClassMethodMember(info, member),
);
}
if (info.staticMethods) for (const [name, entry] of info.staticMethods) {
@@ -4167,6 +4192,7 @@ export class Lowerer {
register(`%${cName}.set:${prop}`, [], () => this.throwingSetterFn(info, prop));
}
};
for (const name of collectionEdges) this.onEdge(name);
// Generic instances queued by the bodies above lower here (an instance
// body fires edges of its own and can queue further instances of
// either kind — function instances and class instantiations drain to
@@ -4825,6 +4851,11 @@ export class Lowerer {
}
typeOf(node: ts.Node): ts.Type {
// A checked JS method receives its actual call-site receiver. Its
// declaration's inferred class type must not turn property reads back
// into native field loads when the method is borrowed by another object.
if (node.kind === ts.SyntaxKind.ThisKeyword && isJsSourceFile(node.getSourceFile()) &&
this.fnStack.length > 0 && this.resolveThis()?.type.kind === "dyn") return this.checker.getUnknownType();
// Inside an optional-chain body the guarded receiver is typed by its
// NON-NULLISH type (the chain's tag test proved it), so every
// receiver-kind check downstream sees the narrowed arm.
@@ -5353,17 +5384,13 @@ export class Lowerer {
return { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
result: dynUndefinedExpr(expr.loc), type: DYN, loc: expr.loc };
}
// An error-HIERARCHY object (builtin subclass or user `extends
// Error` class) upcasts to the %Error root first — the caughtToDyn
// encoding (scr_dyn_from_error) carries name/message/code and the
// runtime CACHES the identity edge, so `instanceof TypeError` on
// the dyn side still answers exactly (dyn.errInstanceof). Only the
// root spelling was convertible before; the harness passes typed
// errors into untyped helpers constantly.
// Preserve the concrete class so checked property/method dispatch
// discovers user Error subclasses. Runtime boxing selects the live
// view by vtable even when an error first crosses through its base.
if (expr.type.kind === "object" && this.errorHierarchyClassOf(expr.type.className)) {
return {
kind: "dynFrom",
value: this.upcastTo(expr, "%Error"),
value: expr,
type: DYN,
loc: expr.loc,
};
@@ -5793,6 +5820,7 @@ export class Lowerer {
const sameFamily =
(src.kind === "record" && arm.kind === "record") ||
(src.kind === "array" && arm.kind === "array") ||
(src.kind === "func" && arm.kind === "func") ||
(src.kind === "object" && arm.kind === "object") ||
// Tuples already lift into ordinary array slots. Consider that
// same conversion when the array is an arm of a union too.
@@ -8137,7 +8165,8 @@ export class Lowerer {
return sym?.name === name && this.isStdlibSymbol(sym);
});
if (ts.isObjectLiteralExpression(x)) {
if (isJsSourceFile(x.getSourceFile()) || builtinOption("RequestInit") || builtinOption("ErrorOptions")) {
if (isJsSourceFile(x.getSourceFile()) || builtinOption("RequestInit") || builtinOption("ErrorOptions") ||
!this.mapTypeOf(this.typeOf(x))) {
return lowerDynObjectLiteral(this, x);
}
}
@@ -8448,12 +8477,13 @@ export class Lowerer {
* symbol reports eagerly (nothing could ever reference it). */
collectDeferring(symbolOf: () => ts.Symbol | undefined, collect: () => void): ts.Symbol | null {
const sink: ScrDiagnostic[] = [];
const previousSink = this.diagSink;
this.diagSink = sink;
try {
collect();
return null;
} catch (e) {
this.diagSink = null;
this.diagSink = previousSink;
const symbol = (() => {
// symbolOf queries the checker too — a second panic must not
// escape the fence that is handling the first.
@@ -8485,7 +8515,7 @@ export class Lowerer {
this.deferredDiags.set(symbol, list);
return symbol;
} finally {
this.diagSink = null;
this.diagSink = previousSink;
}
}
@@ -8691,6 +8721,8 @@ export class Lowerer {
* BEFORE that statement (JS's order for the supported whole-initializer
* positions). */
readonly pendingClassExprInits: IrStmt[] = [];
readonly computedClassBases = new Map<string, { value: IrExpr; classInfo: ClassInfo }>();
readonly computedCallableBases = new Map<ts.ClassLikeDeclaration, IrExpr>();
/** Discovery hook: registers a just-collected expression class's member
* bodies as worklist units (the units map is otherwise built before
* lowering starts). Null in the emit pass. */
@@ -8790,11 +8822,7 @@ export class Lowerer {
// Computed method names resolve exactly like collection did
// (classMemberNameOf — folded keys and the sym:iterator slot);
// unresolvable ones never collected, so they skip here too.
const baseName = ts.isMethodDeclaration(fnLike)
? classMemberNameOf(this, fnLike.name)
: ts.isIdentifier(fnLike.name) || ts.isPrivateIdentifier(fnLike.name)
? fnLike.name.text
: null;
const baseName = classMemberNameOf(this, fnLike.name);
if (baseName === null) continue;
const mName = ts.isMethodDeclaration(fnLike) ? baseName : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${baseName}`;
if (!info.methods.get(mName) || !fnLike.body) continue;
@@ -9183,9 +9211,7 @@ export class Lowerer {
});
if (resolved.error !== null) {
const location = blame ?? (symbol === undefined ? undefined : this.checker.declarationsOf(symbol)[0]) ?? this.entry;
const message = resolved.error === "caught"
? "closures capturing catch bindings (narrow into a typed local first)"
: `the binding '${resolved.origin!.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`;
const message = `the binding '${resolved.origin!.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`;
this.unsupported("SC1090", location, message);
}
if (resolved.local !== null) return resolved.local;
@@ -627,6 +627,8 @@ function exactValueParams(...types: IrType[]): BuiltinValueParam[] {
/** Object helpers whose checked-native ABI is also their stored value ABI.
* These adapters operate on the live checked object/property table. */
export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined> = {
is: { fn: "dyn.sameValue", params: [DYN, DYN], result: BOOL, valueParams: exactValueParams(DYN, DYN) },
create: { fn: "dyn.objCreateWithProperties", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
defineProperty: { fn: "dyn.defineProperty", params: [DYN, DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN, DYN) },
getOwnPropertyDescriptor: { fn: "dyn.getOwnPropertyDescriptor", params: [DYN, DYN], result: DYN, valueParams: exactValueParams(DYN, DYN) },
getOwnPropertyDescriptors: { fn: "dyn.getOwnPropertyDescriptors", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
@@ -636,6 +638,8 @@ export const OBJECT_CALLABLE_VALUES: Record<string, BuiltinModuleFn | undefined>
entries: { fn: "dyn.objEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
fromEntries: { fn: "dyn.fromEntries", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
getOwnPropertySymbols: { fn: "dyn.getOwnPropertySymbols", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
getOwnPropertyNames: { fn: "dyn.getOwnPropertyNames", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
getPrototypeOf: { fn: "dyn.getPrototype", params: [DYN], result: DYN, valueParams: exactValueParams(DYN) },
};
/** The lowerable surface of the supported node builtin modules, keyed by
@@ -59,11 +59,11 @@ test("record and union IDs have independent identities", () => {
});
test("fresh indexes observe later class registration and type-table completion", () => {
let shape: IrRecordShape | undefined;
const table: { shape?: IrRecordShape } = {};
const classes = new Set<string>();
const fresh = () => new UnregisteredClassTypes(() => shape, () => undefined, (name) => classes.has(name));
const fresh = () => new UnregisteredClassTypes(() => table.shape, () => undefined, (name) => classes.has(name));
expect(fresh().has(record("r"))).toBe(false);
shape = { id: "r", fields: [{ name: "value", type: missing }] };
table.shape = { id: "r", fields: [{ name: "value", type: missing }] };
expect(fresh().has(record("r"))).toBe(true);
classes.add("Missing");
expect(fresh().has(record("r"))).toBe(false);
+36 -16
View File
@@ -522,20 +522,19 @@ export function entryPackageFilePredicate(entryPath: string): (file: string) =>
};
}
/** maxNodeModuleJsDepth omits even relative JS imports within an installed
* entry's own package. Add those sources as roots to the existing discovery
* fixpoint without raising the checker depth for other packages. */
/** maxNodeModuleJsDepth can omit relative imports within an admitted
* package. Discover its runtime sources to a fixpoint so the checker and
* module resolver see the same graph regardless of import traversal order. */
function entryPackageProgramRoots7(program: ts.Program, entryPath: string): string[] {
if (!isNodeModulesPath(entryPath)) return [];
const belongsToEntry = entryPackageFilePredicate(entryPath);
const roots = new Set<string>();
for (const sf of program.getSourceFiles()) {
if (sf.isDeclarationFile || !belongsToEntry(sf.fileName)) continue;
if (sf.isDeclarationFile || (!belongsToEntry(sf.fileName) && npmStaticPackageOfPath(sf.fileName) === null)) continue;
for (const specifier of sf.imports) {
if (!ts.isStringLiteralLike(specifier)) continue;
const target = resolveProjectModule(sf.fileName, specifier.text);
if (
target !== null && belongsToEntry(target) && isJsSourceFileName(target) &&
target !== null && (belongsToEntry(target) || npmStaticPackageOfPath(target) !== null) && isJsSourceFileName(target) &&
program.getSourceFile(target) === undefined
) roots.add(target);
}
@@ -771,12 +770,28 @@ export function loadProgram(
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
try {
const load = loadProgram7(host, entryPath, externalTypes);
let authoredHost: ts.Ts7Host | null = null;
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
return {
...load,
services,
projectWorld: npmStaticPackages.length === 0 ? load.projectWorld : () => {
// Check authored code against the package's published declarations.
// Runtime bodies still lower from JavaScript in the original world;
// inference there must not manufacture errors in valid user code.
if (!authoredLoad) {
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
}
return authoredLoad.projectWorld();
},
dispose: () => {
try { load.disposeAll(); }
finally { host.close(); }
try { authoredLoad?.disposeAll(); }
finally {
authoredHost?.close();
try { load.disposeAll(); }
finally { host.close(); }
}
},
};
} catch (error) {
@@ -810,7 +825,10 @@ export function checkPreflight<T extends LoadResult>(load: T): ScrDiagnostic[] {
* assignments in CJS classes (`this[kSym] = v` under `module.exports =
* Class`) — 5.9.3 synthesized expando symbols, tsgo trips its
* redeclaration check instead (the corpus's countdown fixture). */
const TS7_JS_RELAXED_EXTRA: ReadonlySet<number> = new Set([2309, 2323]);
// Ordinary JavaScript functions are constructable even when the checker
// retains only their call signature after a factory return. Heritage
// lowering separately verifies the actual constructor implementation.
const TS7_JS_RELAXED_EXTRA: ReadonlySet<number> = new Set([2309, 2323, 2507]);
function suppressedJsStrictness7(d: ts.Diagnostic): boolean {
const file = d.fileName;
@@ -2055,10 +2073,12 @@ export function makeCycleAdmission(
* program, or null (unresolvable / outside the program). Exported (as
* resolveImport) for the lowering: CommonJS require statements lower to
* guarded %init calls of exactly the module preflight resolved here. */
function resolveImport7(program: ts.Program, from: ts.SourceFile, specifier: string): ts.SourceFile | null {
function resolveImport7(program: ts.Program, from: ts.SourceFile, specifier: string, resolutionKind: "import" | "require" = "import"): ts.SourceFile | null {
const resolved = resolveProjectModule(from.fileName, specifier);
if (resolved === null) return null;
return program.getSourceFile(resolved) ?? null;
if (resolved !== null) return program.getSourceFile(resolved) ?? null;
const npm = resolveNpmImport7(from.fileName, specifier, resolutionKind);
return npm && isNpmStaticPackage(npm.packageName)
? npmStaticProgramDep(program, npm.packageName, npm.typesFile) : null;
}
/** An import that resolves into node_modules: the package's shipped .d.ts
@@ -2433,7 +2453,7 @@ function preflight7(load: LoadResult): {
entry,
programFiles,
[...createRequireProgramRoots7(program), ...forkTargetPaths(program, program.getSourceFiles())],
(sf, spec, resolutionKind) => resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec, resolutionKind),
(sf, spec, resolutionKind) => resolveImport7(program, sf, spec, resolutionKind) ?? npmStaticDepSf7(program, sf, spec, resolutionKind),
)
: programFiles;
program.getTypeChecker().prefetchSourceFileStructures(userFiles);
@@ -2889,7 +2909,7 @@ function preflight7(load: LoadResult): {
}
}
const isRelative = isRelativeSpecifier(req.spec);
let dep = resolveImport7(program, sf, req.spec);
let dep = resolveImport7(program, sf, req.spec, "require");
if (dep === null && !isRelative) {
// --npm-static: a require() of an OPTED-IN package is a
// program-module edge exactly like the import-declaration
@@ -2972,7 +2992,7 @@ function preflight7(load: LoadResult): {
if (load.externalTypes.has(spec)) {
continue;
}
const projectDep = resolveImport7(program, sf, spec);
const projectDep = resolveImport7(program, sf, spec, "require");
if (projectDep !== null) {
if (projectDep.fileName.endsWith(".json")) {
if (bindingKind === "createRequire") continue;
@@ -3166,7 +3186,7 @@ function cjsNamedImportLinkCheck(
// Reexport targets union in only when they resolve to CommonJS program
// files (Node's cjsPreparseModuleExports rule).
const resolveCjsDep = (from: ts.SourceFile, spec: string): ts.SourceFile | null => {
const dep = resolveImport7(program, from, spec) ?? npmStaticDepSf7(program, from, spec, "require");
const dep = resolveImport7(program, from, spec, "require") ?? npmStaticDepSf7(program, from, spec, "require");
return dep !== null && isCjsJsFile7(dep, program) ? dep : null;
};
const visible = (dep: ts.SourceFile, name: string): boolean =>
+27 -10
View File
@@ -30,9 +30,18 @@ export function jsOpenObjectType(
// 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;
// An unannotated JavaScript parameter's object default is a fallback,
// not a closed class contract. Published code routinely passes another
// implementation of the same protocol through a boxed callable.
if (parameter?.initializer && !parameter.type &&
present?.some((arm) => arm.kind === "object") &&
arms?.every((arm) => canConvertToDyn(arm, (id) => shapes.get(id), (id) => unions.get(id)))) 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;
const unitOnly = (field: IrType): boolean => isUnitType(field) || field.kind === "void" ||
field.kind === "union" && unions.get(field.unionId)?.arms.every(isUnitType) === true;
return shape && !shape.tuple && !shape.indexValue &&
(shape.fields.length === 0 || shape.fields.some((field) => unitOnly(field.type))) ? DYN : type;
}
/** The ambient TYPE names of the fetch slice. Under --dynamic their
@@ -859,11 +868,8 @@ export interface TypeMapperCtx {
* sentinel; any/unknown ride dyn; undefined-only yields have no C value
* form (null); else the mapped type.
* - return: void/undefined/never carry no value (VOID — the done-value is
* the undefined arm); any/unknown are DEFAULTS (`Generator<number>` is
* Generator<number, any, any>), and a defaulted return channel means
* "no modeled return value" — VOID, not dyn (a dyn return channel would
* force the yield channel dyn too; bodies that `return v` under a
* defaulted TReturn keep their fence at the return site).
* the undefined arm); any/unknown carry checked values. A defaulted or
* inferred return channel can still return a value and must preserve it.
* - next: void/undefined/never mean valueless resumes (the undefined
* unit); any/unknown ride dyn; else the mapped type (`.next(v)` then
* requires its argument — fenced at the call site).
@@ -886,9 +892,9 @@ function genChannels(
if (yieldT?.kind === "void" || (yieldT && isUnitType(yieldT))) yieldT = null;
}
if (!yieldT) return null;
const retT = retTs === undefined || retTs.flags & (UNITISH | ANYISH)
const retT = retTs === undefined || retTs.flags & UNITISH
? VOID
: mapType(retTs, ctx);
: retTs.flags & ANYISH ? DYN : mapType(retTs, ctx);
if (!retT || (retT.kind !== "void" && isUnitType(retT))) return null;
const nextT = nextTs === undefined || nextTs.flags & UNITISH
? UNDEFINED_T
@@ -1326,7 +1332,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
}
const fields: { name: string; type: IrType }[] = [];
for (let i = 0; i < args.length; i++) {
let et = mapType(args[i]!, ctx);
let et = !ctx.dynamic && (args[i]!.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(args[i]!, ctx);
// A unit-only element (`[number, undefined]`) rides the unit-only
// union, the record-field rule.
if (et?.kind === "void" && isUnitOnlyTsType(args[i]!, ctx.resolveTypeParam)) et = unitOnlyUnion(unions);
@@ -2339,6 +2345,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (!inner) return null;
return { kind: "promise", inner };
}
// Native collection iterators use checked runtime iterator objects.
if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN;
// Generator<T, TReturn, TNext>, AsyncGenerator<T, TReturn, TNext> (and
// the lib's IterableIterator<T, ...>, the older sync annotation spelling):
// the generator kind. Channel normalization keeps the runtime honest:
@@ -3759,7 +3767,12 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean {
if (isMappedShape(t)) return true;
const tSym = t.getSymbol();
const decls = tSym ? checker.declarationsOf(tSym) : undefined;
if (!decls || decls.length === 0) return false;
if (!decls || decls.length === 0) {
// Inferred destructuring parameters can have a synthesized anonymous
// shape with no declaration or member symbols to trace to a source.
return checker.getPropertiesOfType(t).every((property) => checker.declarationsOf(property).every((decl) =>
!decl.getSourceFile().isDeclarationFile || ctx.isExternalTypeFile(decl.getSourceFile())));
}
return !decls.some((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
@@ -3988,6 +4001,9 @@ function mapRecordTypeInner(widened: ts.Type, ctx: TypeMapperCtx): IrType | Reco
const computed = widened.isIntersectionType() || isMappedShape(widened);
{
const props = checker.getPropertiesOfType(widened);
// Symbol-keyed records use the checked object's descriptor table. A
// checker-internal __@ name is not a string field in a native struct.
if (props.some((property) => property.name.startsWith("__@"))) return DYN;
// A computed shape with NO members is not a real empty record: inside a
// generic body `Partial<T>` resolves to no members at all (keyof T is
// still unknown there) — interning `{}` would be silently wrong. The
@@ -4105,6 +4121,7 @@ function mapRecordTypeInner(widened: ts.Type, ctx: TypeMapperCtx): IrType | Reco
if ((pt === null || pt.kind === "jsval") && (fieldTs.flags & ts.TypeFlags.Any) !== 0 && constAssertedEmptyArrayProp(p, ctx)) {
pt = arrayOf(unitOnlyUnion(ctx.unions));
}
if (pt === null && !ctx.dynamic && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN;
// Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union
// absent-field idiom — and `{ p: undefined }` spellings): the
// unit-only union. The runtime value is the interned unit, JSON
+2
View File
@@ -74,6 +74,7 @@ export function dynDesc(
case "nullT": return "null";
case "undefinedT": return "undefined";
case "dyn": return "unknown";
case "promise": return "Promise";
case "bytes": return BYTES_ELEMENT_NAME[t.elem];
case "object": return t.className.replace(/^%/, "");
case "union": {
@@ -88,6 +89,7 @@ export function dynDesc(
case "set": return "Set";
case "regex": return "RegExp";
case "url": return "URL";
case "date": return "Date";
default: {
const handle = DYN_HANDLE_KINDS.get(t.kind);
if (handle) return handle.cls;
+40
View File
@@ -123,6 +123,46 @@ describe("native bigint checked storage", () => {
});
});
describe("checked conversion graph traversal", () => {
test.each([
{ label: "boxing", convert: canConvertToDyn },
{ label: "checking", convert: canDynCheckTo },
])("$label reuses shared record and union subgraphs", ({ convert }) => {
const records = new Map<string, IrRecordShape>();
const unions = new Map<string, IrUnionDef>();
let type: IrType = { kind: "symbol" };
for (let i = 0; i < 16; i++) {
const id = `r${i}`, unionId = `u${i}`;
records.set(id, { id, fields: [{ name: "left", type }, { name: "right", type }] });
unions.set(unionId, { id: unionId, arms: [{ kind: "record", shapeId: id }, { kind: "undefinedT" }] });
type = { kind: "union", unionId };
}
let lookups = 0;
expect(convert(type,
(id) => { lookups++; return records.get(id); },
(id) => { lookups++; return unions.get(id); },
)).toBe(true);
expect(lookups).toBeLessThan(1000);
});
test("failed recursive branches discard dependent conversion results", () => {
const records = new Map<string, IrRecordShape>([
["parent", { id: "parent", fields: [
{ name: "child", type: { kind: "record", shapeId: "child" } },
{ name: "unsupported", type: { kind: "jsval" } },
] }],
["child", { id: "child", fields: [
{ name: "parent", type: { kind: "record", shapeId: "parent" } },
{ name: "symbol", type: { kind: "symbol" } },
] }],
]);
const visiting = new Set<string>();
const getRecord = (id: string) => records.get(id);
expect(canConvertToDyn({ kind: "record", shapeId: "parent" }, getRecord, () => undefined, visiting)).toBe(false);
expect(canConvertToDyn({ kind: "record", shapeId: "child" }, getRecord, () => undefined, visiting)).toBe(false);
});
});
describe("runtime feature snapshots", () => {
const loc = { file: "features.ts", start: 0, end: 0 };
+127 -36
View File
@@ -300,8 +300,8 @@ export type IrType =
* JS-exact semantics; exits back to static types are validated (jsExit).
* This kind exists only with the dynamic option. */
| { kind: "jsval" }
/** A catch binding — the type of `catch (e)`'s local, and NOTHING else
* (never params, returns, fields, arms, elements, globals, captures).
/** A catch binding or its closure capture (never parameters, returns,
* fields, arms, elements or globals).
* Runtime representation is a refcounted snapshot box (ScrCaught) holding
* the taken exception: a kind tag plus the payload. Even NARROWER than
* dyn: the only expressions a caught value may appear in are `caughtTest`
@@ -1140,6 +1140,8 @@ export interface IrClassDef {
localCaptures?: IrParam[];
/** Capture holding the evaluated local base constructor, if any. */
localBaseCapture?: number;
/** Evaluated module-scope heritage constructor for a factory-produced base. */
baseValueGlobal?: string;
/** The JS-observable `.name` of the class (the runtime class object's
* name string, and what `C.name` folds to). Differs from `name` because
* IR names are program-qualified (`%m1.C`, `%cx…` for class
@@ -1152,6 +1154,10 @@ export interface IrClassDef {
/** Zero-argument native helper returning the shared prototype data object.
* Used when materializing an instance's own-property view. */
prototypeDataHelper?: string;
/** Native instance to its actual prototype, including factory captures and subclasses. */
instancePrototypeHelper?: string;
/** Stable module symbol identities for public instance layout fields. */
symbolFields?: { field: string; globalId: string }[];
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so
* backends emit no definitions for it — only the preorder-interval
@@ -2114,6 +2120,7 @@ export type IrLibFn =
| "dyn.globalSymbolDelete"
/** Exact native class capsule identity, without materializing its fields. */
| "dyn.typedRefIs"
| "dyn.classIs"
/** Destructuring pack over a dyn source — `const [a, b] = d`, a
* destructured dyn callback param (args: the source and the STATIC
* TypeError spelling, "" when the source has none — both borrowed;
@@ -2415,6 +2422,8 @@ export type IrLibFn =
* result; never throws. (Union-armed operands take unionEq's sameValue
* flag instead — this is the both-f64 fast path.) */
| "num.sameValue"
/** SameValue over checked values, including NaN, signed zero and reference identity. */
| "dyn.sameValue"
/** `new Intl.NumberFormat("en-US").format(x)` and
* `x.toLocaleString("en-US")` with DEFAULT options — the one locale
* whose data the runtime embeds (Node's default-build locale): decimal
@@ -3661,6 +3670,7 @@ export type IrLibFn =
* (the strict-mode plain-call answer, the old constant). Zero args →
* dyn (+1). Never throws. */
| "dyn.this"
| "dyn.generatorThis"
/** process.execPath: the compiled binary's own resolved absolute path
* (one interned string, +1 per read) — the honest answer where Node's
* is the node executable's (SEMANTICS.md divergence 12). Never throws. */
@@ -3874,7 +3884,8 @@ export type IrLibFn =
* receiver (already allocated by the derived class's new) + borrowed
* message, void; the receiver's type names the builtin class whose name
* field to stamp. error.toString: borrowed `%Error`-typed receiver, +1
* string in Node's "name: message" shape. None of the three throws. */
* string in Node's "name: message" shape. Formatting can invoke a user
* message accessor, which can throw. */
| "error.new"
/** ECMAScript constructors with raw checked-dynamic message/options.
* Options retain cause presence; constructor calls borrow their args.
@@ -4014,8 +4025,12 @@ export type IrLibFn =
* only; --dynamic routes Object.create through the engine instead. */
| "dyn.objCreateNullProto"
| "dyn.arrayPrototype"
| "dyn.objectPrototype"
| "dyn.functionApply"
| "dyn.builtinMethod"
| "dyn.classPrototype"
| "dyn.classBasePrototype"
| "dyn.classInherit"
| "dyn.classSuper"
| "dyn.assignPrototype"
| "dyn.objCreate"
@@ -4023,6 +4038,7 @@ export type IrLibFn =
| "dyn.getPrototype"
| "dyn.setPrototype"
| "dyn.getOwnPropertyNames"
| "dyn.ownKeys"
| "dyn.getOwnPropertySymbols"
| "dyn.getOwnPropertyDescriptors"
| "dyn.preventExtensions"
@@ -5547,7 +5563,7 @@ export type IrExpr =
* (undefined-padded past the end) — the empty pattern passes count 0
* and uses only the validation. Value is borrowed; the result is owned
* (+1). */
| { kind: "dynIterN"; value: IrExpr; count: number; type: IrType; loc: SrcLoc }
| { kind: "dynIterN"; value: IrExpr; count: number; notIterableMessage?: string; type: IrType; loc: SrcLoc }
/** The OVERFLOW key list of an index-signature record, in JS OWN-KEY
* order (canonical array indices ascending first, then insertion order —
* the runtime's scr_map_keys_js_order): a fresh string[] snapshot, the
@@ -6231,7 +6247,7 @@ export function canMarshalTypedFuncIntoIsland(
* through a dyn value), properties ON function values, and params/results
* outside the conversion domains (Maps, class instances, ...). Promises
* CONVERT in (canConvertToDyn's promise arm — an async dyn-boxed closure's
* return) but do not check OUT (`u as Promise<T>` stays fenced).
* return); checking OUT preserves the boxed Promise<unknown> payload ABI.
*/
/** The runtime HANDLE kinds that cross the checked-dynamic boundary as
@@ -6300,6 +6316,7 @@ export function canConvertToDyn(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string> = new Set(),
): boolean {
if (isJsonSafeType(t, getRecord, getUnion)) return true;
// numeric typed arrays and boxable functions are dyn kinds the walker boxes
@@ -6307,16 +6324,16 @@ export function canConvertToDyn(
// in records/arrays/unions. isJsonSafeType rejects them, but dynFrom
// needs only that the walker can build the dyn value, so this composite
// fold extends the JSON-safe core.
if (canBoxDynComposite(t, getRecord, getUnion)) return true;
if (canBoxDynComposite(t, getRecord, getUnion, visiting)) return true;
if (t.kind === "bytes") return true;
// %Error converts as the checked-dynamic tree's error encoding ({%error, name, message,
// Built-in errors convert as the checked-dynamic tree's error encoding ({%error, name, message,
// code?} — the caughtToDyn shape, scr_dyn_from_error): the dyn 'error'
// listener boundary (a mustCall-wrapped handler receiving the payload).
if (t.kind === "object" && t.className === "%Error") return true;
if (t.kind === "object" && RUNTIME_ERROR_CLASSES.has(t.className)) return true;
if (isDynTypedRefType(t)) return true;
if (t.kind === "generator") return true;
if (t.kind === "classval") return true;
if (t.kind === "func") return canBoxFuncIntoDyn(t, getRecord, getUnion);
if (t.kind === "func") return canBoxFuncIntoDyn(t, getRecord, getUnion, visiting);
if (DYN_HANDLE_KINDS.has(t.kind)) return true;
// Promises box by REFERENCE (SCR_DYN_PROMISE): promise<dyn> carries its
// ScrPromise directly (the payload is already a dyn value), any other
@@ -6328,7 +6345,7 @@ export function canConvertToDyn(
return (
t.inner.kind === "dyn" ||
t.inner.kind === "void" ||
canConvertToDyn(t.inner, getRecord, getUnion)
canConvertToDyn(t.inner, getRecord, getUnion, visiting)
);
}
if (t.kind === "union") {
@@ -6340,8 +6357,8 @@ export function canConvertToDyn(
return !!def && def.arms.every((a) =>
a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) ||
isDynTypedRefType(a) || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) ||
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion)) ||
(a.kind === "promise" && canConvertToDyn(a, getRecord, getUnion)),
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion, visiting)) ||
(a.kind === "promise" && canConvertToDyn(a, getRecord, getUnion, visiting)),
);
}
return false;
@@ -6350,14 +6367,28 @@ export function canConvertToDyn(
/** The composite extension of the dynFrom domain: JSON-safe scalars plus
* numeric typed arrays and boxable functions anywhere, recursing through records
* (fields + index value), arrays, and unit-armed unions — exactly the
* sc_td_* walker's capability. Returns false for a composite carrying a
* kind the walker cannot box (Maps or handles nested in a record); those
* still fence. */
* sc_td_* walker's capability. Map and Set payloads must also support
* checked extraction back into native storage. Other unsupported nested
* kinds retain their conversion fence. */
function canBoxDynComposite(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string> = new Set(),
): boolean {
const key = `boxed:${typeKey(t)}`;
if (visiting.has(key)) return true;
const result = canBoxDynCompositeAt(t, getRecord, getUnion, visiting);
if (result) visiting.add(key);
else clearDynConversionResults(visiting);
return result;
}
function canBoxDynCompositeAt(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string>,
): boolean {
switch (t.kind) {
case "f64":
@@ -6375,13 +6406,14 @@ function canBoxDynComposite(
case "url":
return true;
case "func":
return canBoxFuncIntoDyn(t, getRecord, getUnion);
return canBoxFuncIntoDyn(t, getRecord, getUnion, visiting);
case "set":
return t.elem.kind === "dyn";
return canConvertToDyn(t.elem, getRecord, getUnion, visiting) && canDynCheckTo(t.elem, getRecord, getUnion, visiting);
case "map":
return t.key.kind === "dyn" && t.value.kind === "dyn";
return canConvertToDyn(t.key, getRecord, getUnion, visiting) && canDynCheckTo(t.key, getRecord, getUnion, visiting) &&
canConvertToDyn(t.value, getRecord, getUnion, visiting) && canDynCheckTo(t.value, getRecord, getUnion, visiting);
case "object":
return t.className === "%Error" || isDynTypedRefType(t);
return RUNTIME_ERROR_CLASSES.has(t.className) || isDynTypedRefType(t);
case "array":
return canBoxDynComposite(t.elem, getRecord, getUnion, visiting);
case "record": {
@@ -6391,20 +6423,28 @@ function canBoxDynComposite(
// Recursive shapes answer coinductively, like isJsonSafeType.
if (visiting.has(t.shapeId)) return true;
visiting.add(t.shapeId);
for (const field of shape.fields) {
if (!canBoxDynComposite(field.type, getRecord, getUnion, visiting)) return false;
try {
for (const field of shape.fields) {
if (!canBoxDynComposite(field.type, getRecord, getUnion, visiting)) return false;
}
return !shape.indexValue || canBoxDynComposite(shape.indexValue, getRecord, getUnion, visiting);
} finally {
visiting.delete(t.shapeId);
}
return !shape.indexValue || canBoxDynComposite(shape.indexValue, getRecord, getUnion, visiting);
}
case "union": {
const def = getUnion(t.unionId);
if (!def) return false;
if (visiting.has(t.unionId)) return true;
visiting.add(t.unionId);
for (const arm of def.arms) {
if (!canBoxDynComposite(arm, getRecord, getUnion, visiting)) return false;
try {
for (const arm of def.arms) {
if (!canBoxDynComposite(arm, getRecord, getUnion, visiting)) return false;
}
return true;
} finally {
visiting.delete(t.unionId);
}
return true;
}
default:
return false;
@@ -6420,6 +6460,30 @@ export function canDynCheckTo(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string> = new Set(),
): boolean {
const key = `checked:${typeKey(t)}`;
if (visiting.has(key)) return true;
const result = canDynCheckToAt(t, getRecord, getUnion, visiting);
if (result) visiting.add(key);
else clearDynConversionResults(visiting);
return result;
}
/** Reuse completed subgraphs within one conversion query. A failed branch
* invalidates successes that may depend on a coinductive back-edge to it;
* active recursion keys remain until their owning calls unwind. */
function clearDynConversionResults(visiting: Set<string>): void {
for (const key of visiting) {
if (key.startsWith("boxed:") || key.startsWith("checked:")) visiting.delete(key);
}
}
function canDynCheckToAt(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string>,
): boolean {
// Unknown fields keep an owned dyn subtree; checking the surrounding
// record/array still validates its layout. This is broader than the
@@ -6427,24 +6491,43 @@ export function canDynCheckTo(
// serializable. Backends already retain dyn fields and fill missing
// unknown record fields with the undefined value.
if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true;
if (t.kind === "bigint" || t.kind === "symbol" || t.kind === "set" && t.elem.kind === "dyn") return true;
if (t.kind === "map" && t.key.kind === "dyn" && t.value.kind === "dyn") return true;
if (t.kind === "bigint" || t.kind === "symbol" || t.kind === "date") return true;
if (t.kind === "map" || t.kind === "set") return canBoxDynComposite(t, getRecord, getUnion, visiting);
if (t.kind === "bytes") return true;
if (t.kind === "classval") return true;
if (t.kind === "generator") return true;
if (t.kind === "promise") return t.inner.kind === "dyn";
if (t.kind === "object" && t.className === "%Error") return true;
// Native class capsules already support checked extraction at ordinary
// boundaries. Callable adapters use the same identity/brand check.
if (isDynTypedRefType(t)) return true;
if (t.kind === "array" && isDynTypedRefType(t.elem)) return true;
if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion);
if (t.kind === "array") {
const key = typeKey(t);
if (visiting.has(key)) return false;
visiting.add(key);
const result = canDynCheckTo(t.elem, getRecord, getUnion, visiting);
visiting.delete(key);
return result;
}
if (t.kind === "record") {
const key = typeKey(t);
if (visiting.has(key)) return false;
const shape = getRecord(t.shapeId);
if (!shape || shapeHasAccessorSlots(shape)) return false;
visiting.add(key);
const result = shape.fields.every((field) => canDynCheckTo(field.type, getRecord, getUnion, visiting)) &&
(!shape.indexValue || canDynCheckTo(shape.indexValue, getRecord, getUnion, visiting));
visiting.delete(key);
return result;
}
if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion, visiting);
if (DYN_HANDLE_KINDS.has(t.kind)) return true;
if (t.kind === "union") {
const def = getUnion(t.unionId);
// Optional native callbacks and handles retain the same checked
// conversion as their bare value. The union matcher selects the arm
// before its adapter/extractor runs.
return !!def && def.arms.every((a) => isUnitType(a) || (a.kind !== "union" && canDynCheckTo(a, getRecord, getUnion)));
return !!def && def.arms.every((a) => isUnitType(a) || (a.kind !== "union" && canDynCheckTo(a, getRecord, getUnion, visiting)));
}
return false;
}
@@ -6457,6 +6540,7 @@ export function canBoxFuncIntoDyn(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string> = new Set(),
): boolean {
return (
t.kind === "func" &&
@@ -6469,11 +6553,11 @@ export function canBoxFuncIntoDyn(
// thunk (wrapped cells unwrap by reference, dyn data deep-copies) —
// the checker-'any' callback params of the routed-dispatch lane
// (`bag.list.map((x) => ...)` with x typed any).
t.params.every((p) => p.kind === "dyn" || p.kind === "jsval" || canDynCheckTo(p, getRecord, getUnion)) &&
t.params.every((p) => p.kind === "dyn" || p.kind === "jsval" || canDynCheckTo(p, getRecord, getUnion, visiting)) &&
// A jsval return converts through the by-reference wrap
// (dynFromJsval — the thunk's result conversion), so engine-returning
// callbacks box too: the routed-dispatch lane's flatMap shape.
(t.ret.kind === "void" || t.ret.kind === "dyn" || t.ret.kind === "jsval" || canConvertToDyn(t.ret, getRecord, getUnion))
(t.ret.kind === "void" || t.ret.kind === "dyn" || t.ret.kind === "jsval" || canConvertToDyn(t.ret, getRecord, getUnion, visiting))
);
}
@@ -6485,14 +6569,15 @@ export function canAdaptDynFuncTo(
t: IrType,
getRecord: (shapeId: string) => IrRecordShape | undefined,
getUnion: (unionId: string) => IrUnionDef | undefined,
visiting: Set<string> = new Set(),
): boolean {
return (
t.kind === "func" &&
// Checked rest and arguments packs retain all actual arguments. Typed
// and island rest ABIs still require their own conversion plan.
(t.rest !== true || t.restAbi === undefined) &&
t.params.every((p) => p.kind === "dyn" || canConvertToDyn(p, getRecord, getUnion)) &&
(t.ret.kind === "void" || t.ret.kind === "dyn" || canDynCheckTo(t.ret, getRecord, getUnion))
t.params.every((p) => p.kind === "dyn" || canConvertToDyn(p, getRecord, getUnion, visiting)) &&
(t.ret.kind === "void" || t.ret.kind === "dyn" || canDynCheckTo(t.ret, getRecord, getUnion, visiting))
);
}
@@ -6615,7 +6700,9 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
processEvents: false, emitter: (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
zlib: mod.embedded?.edges.some((e) => e.to === "node:zlib") ?? false,
dc: false, assert: false, dynInvoke: false, dynAsync: false, inspect: false,
dc: false, assert: false, dynInvoke: false,
dynAsync: mod.functions.some((fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn"),
inspect: false,
childProcess: false, net: false, symbol: false, bigint: false, searchParams: false,
qs: false, parseArgs: false, fsWatch: false, nodeTest: false, dgram: false,
http: false, http2: false, tls: false, tlsCa: false,
@@ -6636,7 +6723,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
if (fn.startsWith("zlib.")) features.zlib = true;
if (fn.startsWith("dc.")) features.dc = true;
if (fn.startsWith("assert.")) features.assert = true;
if (fn === "dyn.defineProps" || fn === "dyn.defineProperty" || fn === "dyn.objCreateWithProperties" || fn === "dyn.arrayProtoCall" || fn === "dyn.arrayPrototype") features.dynInvoke = true;
if (fn === "dyn.defineProps" || fn === "dyn.defineProperty" || fn === "dyn.objCreateWithProperties" || fn === "dyn.arrayProtoCall" || fn === "dyn.arrayPrototype" || fn === "dyn.functionApply" || fn === "dyn.builtinMethod") features.dynInvoke = true;
if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true;
if (fn.startsWith("insp.") || fn === "console.native" || fn === "global.native") features.inspect = true;
if (fn.startsWith("cp.") || fn.startsWith("child.") || fn.startsWith("writer.") || fn.startsWith("spawnRes.") ||
@@ -7507,6 +7594,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// Error messages use the same coercion protocol before installing cause.
"error.newOptions",
"error.ctorOptions",
"error.toString",
"error.stack",
"error.cause",
"error.setCause",
"error.deleteCause",
@@ -7519,6 +7608,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.bitwise",
"dyn.proxyNew",
"dyn.classBasePrototype",
"dyn.classInherit",
"dyn.classSuper",
"dyn.hasKey",
"child.kill",
@@ -7598,6 +7688,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.getPrototype",
"dyn.setPrototype",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
"dyn.getOwnPropertyDescriptors",
"dyn.preventExtensions",
+52 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { BOOL, DYN, F64, NULL_T, STRING, UNDEFINED_T, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
import { BOOL, DYN, F64, NULL_T, STRING, SYMBOL_T, UNDEFINED_T, VOID, arrayOf, mapOf, setOf, type IrExpr, type IrModule, type IrType, type IrUnionDef } from "./ir.js";
import { deserializeModule, serializeModule } from "./serialize.js";
import { validateModule } from "./validate.js";
@@ -62,6 +62,57 @@ test("local classes retain serialized capture slots and fresh identity", () => {
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
});
test("instance prototype helpers require the class receiver ABI", () => {
const mod = localClassModule();
mod.classes![0]!.instancePrototypeHelper = "%Local.prototype";
const receiver: IrType = { kind: "object", className: "Local" };
mod.functions.push({ name: "%Local.prototype", params: [{ localId: "this", name: "this", type: receiver }],
locals: [{ id: "this", name: "this", type: receiver, mutable: false }], returnType: DYN,
body: [{ kind: "return", value: { kind: "dynObjLit", fields: [], type: DYN, loc }, loc }], loc });
expect(validateModule(deserializeModule(serializeModule(mod)))).toEqual([]);
for (const variant of ["missing", "params", "receiver", "return", "captures"]) {
const bad = structuredClone(mod);
const helper = bad.functions.at(-1)!;
if (variant === "missing") bad.functions.pop();
if (variant === "params") helper.params = [];
if (variant === "receiver") helper.params[0]!.type = DYN;
if (variant === "return") helper.returnType = F64;
if (variant === "captures") helper.captures = [];
expect(validateModule(bad).some((error) => error.message.includes("instance prototype helper"))).toBe(true);
}
});
test("computed bases retain their constructor globals after serialization", () => {
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, []);
mod.classes = [{ name: "Base", fields: [], loc }, { name: "Child", base: "Base", fields: [], baseValueGlobal: "%g.computed", loc }];
mod.globals = [{ id: "%g.computed", name: "computed", type: { kind: "classval", className: "Base" }, mutable: false }];
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
expect(validateModule(deserializeModule(serializeModule(mod)))).toEqual([]);
for (const variant of ["missing", "type", "class", "capture"]) {
const bad = structuredClone(mod);
if (variant === "missing") bad.globals = [];
if (variant === "type") bad.globals![0]!.type = DYN;
if (variant === "class") bad.globals![0]!.type = { kind: "classval", className: "Child" };
if (variant === "capture") bad.classes![1]!.localBaseCapture = 0;
expect(validateModule(bad).some((error) => error.message.includes("computed base global"))).toBe(true);
}
});
test("class symbol fields preserve their identity metadata after serialization", () => {
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, []);
mod.classes = [{ name: "Item", fields: [{ name: "sym:key", type: STRING }], symbolFields: [{ field: "sym:key", globalId: "%g.key" }], loc }];
mod.globals = [{ id: "%g.key", name: "key", type: SYMBOL_T, mutable: false }];
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
expect(validateModule(deserializeModule(serializeModule(mod)))).toEqual([]);
for (const variant of ["global", "type", "field"]) {
const bad = structuredClone(mod);
if (variant === "global") bad.globals = [];
if (variant === "type") bad.globals![0]!.type = STRING;
if (variant === "field") bad.classes![0]!.fields = [];
expect(validateModule(bad).some((error) => error.message.includes("symbol field metadata"))).toBe(true);
}
});
test.each(["missing", "unboxed", "type", "slot", "receiver", "closure", "layout", "direct-new"])("local classes reject an invalid %s environment", (variant) => {
const mod = localClassModule();
const ctor = mod.functions[1]!;
+32 -6
View File
@@ -126,6 +126,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL },
"dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID },
"dyn.typedRefIs": { argTypes: [DYN, STRING], result: BOOL },
"dyn.classIs": { argTypes: [DYN, STRING], result: BOOL },
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
"dyn.arrayFromIterator": { argTypes: [DYN], result: DYN },
"dyn.iterator": { argTypes: [DYN, STRING], result: DYN },
@@ -1080,6 +1081,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"process.cwd": { argTypes: [], result: STRING },
"process.pid": { argTypes: [], result: F64 },
"dyn.this": { argTypes: [], result: DYN },
"dyn.generatorThis": { argTypes: [], result: DYN },
"process.getuid": { argTypes: [], result: F64 },
"process.getgid": { argTypes: [], result: F64 },
"process.execPath": { argTypes: [], result: STRING },
@@ -1127,6 +1129,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"class.name": { argTypes: [null], result: STRING },
"error.ctor": { argTypes: [null, STRING], result: VOID },
"error.toString": { argTypes: [null], result: STRING },
"dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL },
"error.stack": { argTypes: [null], result: STRING },
"error.stackLimitGet": { argTypes: [], result: F64 },
"error.stackLimitSet": { argTypes: [F64], result: VOID },
@@ -1181,8 +1184,12 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.assignAll": { argTypes: [DYN, DYN], result: DYN },
"dyn.objCreateNullProto": { argTypes: [], result: DYN },
"dyn.arrayPrototype": { argTypes: [], result: DYN },
"dyn.objectPrototype": { argTypes: [], result: DYN },
"dyn.functionApply": { argTypes: [], result: DYN },
"dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN },
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.classBasePrototype": { argTypes: [DYN], result: DYN },
"dyn.classInherit": { argTypes: [DYN, DYN], result: DYN },
"dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID },
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.objCreate": { argTypes: [DYN], result: DYN },
@@ -1190,6 +1197,7 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
"dyn.getPrototype": { argTypes: [DYN], result: DYN },
"dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN },
"dyn.ownKeys": { argTypes: [DYN], result: DYN },
"dyn.getOwnPropertySymbols": { argTypes: [DYN], result: DYN },
"dyn.getOwnPropertyDescriptors": { argTypes: [DYN], result: DYN },
"dyn.preventExtensions": { argTypes: [DYN], result: DYN },
@@ -1732,6 +1740,12 @@ export function validateModule(mod: IrModule): IrValidationError[] {
errors.push({ message: `class ${cls.name}: invalid local base capture`, loc: cls.loc });
}
}
if (cls.baseValueGlobal !== undefined) {
const global = mod.globals?.find((global) => global.id === cls.baseValueGlobal);
if (cls.localBaseCapture !== undefined || !cls.base || global?.type.kind !== "classval" || global.type.className !== cls.base) {
errors.push({ message: `class ${cls.name}: invalid computed base global`, loc: cls.loc });
}
}
if (cls.jsLength !== undefined && (!Number.isSafeInteger(cls.jsLength) || cls.jsLength < 0)) {
errors.push({ message: `class ${cls.name}: invalid constructor length`, loc: cls.loc });
}
@@ -1741,6 +1755,20 @@ export function validateModule(mod: IrModule): IrValidationError[] {
errors.push({ message: `class ${cls.name}: prototype data helper must be a noncapturing () => dyn function`, loc: cls.loc });
}
}
if (cls.instancePrototypeHelper !== undefined) {
const helper = functionsByName.get(cls.instancePrototypeHelper);
const receiver = helper?.params[0]?.type;
if (!helper || helper.params.length !== 1 || receiver?.kind !== "object" || receiver.className !== cls.name ||
helper.returnType.kind !== "dyn" || helper.captures !== undefined || helper.classCaptures !== undefined) {
errors.push({ message: `class ${cls.name}: instance prototype helper must be a noncapturing (instance) => dyn function`, loc: cls.loc });
}
}
for (const symbol of cls.symbolFields ?? []) {
const global = mod.globals?.find((global) => global.id === symbol.globalId);
if (global?.type.kind !== "symbol" || !cls.fields.some((field) => field.name === symbol.field)) {
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
}
}
const seen = new Set<string>();
for (const f of cls.fields) {
if (seen.has(f.name)) {
@@ -1781,7 +1809,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
errors.push({ message: `class ${cls.name}: undeclared base "${cls.base}"`, loc: cls.loc });
continue;
}
if (base.localCaptures !== undefined && cls.localBaseCapture === undefined) {
if (base.localCaptures !== undefined && cls.localBaseCapture === undefined && cls.baseValueGlobal === undefined) {
errors.push({ message: `class ${cls.name}: local base requires a captured constructor`, loc: cls.loc });
}
const seen = new Set<string>([cls.name]);
@@ -2175,9 +2203,8 @@ function validateFunction(
if (isUnitType(fn.returnType)) {
err(`return type is bare unit type ${fn.returnType.kind}`, fn.loc);
}
// Caught (catch-binding) values are local-only by construction: they can
// never be parameters, returns, or captures (the frontend fences every
// escape; a caught anywhere else is frontend breakage).
// Exception snapshots may be captured, but are never public parameters
// or return values. Their payload crosses those boundaries explicitly.
if (fn.returnType.kind === "caught") err("return type is caught", fn.loc);
for (const p of fn.params) {
if (!locals.has(p.localId)) {
@@ -2190,7 +2217,6 @@ function validateFunction(
if (!local) err(`capture "${c.name}" has no local entry "${c.localId}"`, fn.loc);
else if (!local.boxed) err(`capture local "${c.localId}" is not boxed`, fn.loc);
else if (!typeEquals(local.type, c.type)) err(`capture local "${c.localId}" has the wrong type`, fn.loc);
if (c.type.kind === "caught") err(`capture "${c.name}" is caught-typed`, fn.loc);
}
if (fn.classCaptures !== undefined) {
const self = fn.params[0];
@@ -2908,7 +2934,7 @@ function validateFunction(
!def.arms.every(
(a) =>
a.kind === "undefinedT" || a.kind === "nullT" ||
a.kind === "string" || a.kind === "f64" || a.kind === "bool" ||
a.kind === "string" || a.kind === "f64" || a.kind === "bigint" || a.kind === "bool" ||
(a.kind === "bytes" && a.elem === "u8"),
)
) {
@@ -14,7 +14,6 @@ test.each([
["reviver context", `JSON.parse("1", (key, value, context) => context.source);`, "JSON reviver"],
["reviver rest context", `JSON.parse("1", (...args) => args.length);`, "JSON reviver"],
["reviver arguments context", `JSON.parse("1", function () { return arguments[2].source; });`, "JSON reviver"],
["dynamic gap", `const gap = process.argv[2]; JSON.stringify({ n: 1 }, (key, value) => value, gap);`, "replacer/space"],
] as const)("JSON callback boundary: %s", (_name, source, message) => {
const dir = temp();
try {
@@ -22,7 +21,7 @@ test.each([
writeFileSync(entry, source);
const { coverage } = analyze(entry, { dynamic: false });
expect(coverage.preflightFailed).toBe(false);
expect([...coverage.diagnostics, ...coverage.runtimeFences].some((d) => d.code === "SC2020" && d.message.includes(message)),
expect([...coverage.diagnostics, ...(coverage.runtimeFences ?? [])].some((d) => d.code === "SC2020" && d.message.includes(message)),
JSON.stringify(coverage.diagnostics)).toBe(true);
} finally {
rmSync(dir, { recursive: true, force: true });
@@ -30,7 +30,11 @@ test.each([
test("incompatible JS overrides throw at entry without executing the body", async () => {
const { dir, entry } = fixture(`
class Base { copy(value = 1) { return value; } }
class Child extends Base { copy(value) { console.log('must not execute'); return value; } }
class Child extends Base {
/** @returns {string} */
// @ts-expect-error Exercise the runtime fence for an incompatible JavaScript override.
copy(value) { console.log('must not execute'); return String(value); }
}
const child = new Child();
try { child.copy(2); } catch (error) { console.log(error.message.includes("overriding method 'copy' with a different return type")); }
console.log(new Base().copy());
@@ -8662,6 +8662,12 @@
],
"diags": []
},
"<repo>/tests/corpus/arguments-arrow-capture.mjs": {
"order": [
"<repo>/tests/corpus/arguments-arrow-capture.mjs"
],
"diags": []
},
"<repo>/tests/corpus/arguments-reexport/main.ts": {
"order": [
"<repo>/tests/corpus/arguments-reexport/core.js",
@@ -8682,6 +8688,12 @@
],
"diags": []
},
"<repo>/tests/corpus/array-checked-callback-results.mjs": {
"order": [
"<repo>/tests/corpus/array-checked-callback-results.mjs"
],
"diags": []
},
"<repo>/tests/corpus/array-checked-constructor.js": {
"order": [
"<repo>/tests/corpus/array-checked-constructor.js"
@@ -8700,6 +8712,12 @@
],
"diags": []
},
"<repo>/tests/corpus/array-from-value.mjs": {
"order": [
"<repo>/tests/corpus/array-from-value.mjs"
],
"diags": []
},
"<repo>/tests/corpus/array-isarray-value/main.js": {
"order": [
"<repo>/tests/corpus/array-isarray-value/main.js"
@@ -8772,6 +8790,12 @@
],
"diags": []
},
"<repo>/tests/corpus/builtin-prototype-method-values.mjs": {
"order": [
"<repo>/tests/corpus/builtin-prototype-method-values.mjs"
],
"diags": []
},
"<repo>/tests/corpus/callable-module-members.mjs": {
"order": [
"<repo>/tests/corpus/callable-module-members.mjs"
@@ -8798,6 +8822,12 @@
],
"diags": []
},
"<repo>/tests/corpus/catch-capture.ts": {
"order": [
"<repo>/tests/corpus/catch-capture.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-addition.js": {
"order": [
"<repo>/tests/corpus/checked-addition.js"
@@ -8810,6 +8840,19 @@
],
"diags": []
},
"<repo>/tests/corpus/checked-array-assignment.ts": {
"order": [
"<repo>/tests/corpus/checked-array-assignment.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-array-flat/main.ts": {
"order": [
"<repo>/tests/corpus/checked-array-flat/operations.mjs",
"<repo>/tests/corpus/checked-array-flat/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-bitwise/main.js": {
"order": [
"<repo>/tests/corpus/checked-bitwise/main.js"
@@ -8829,12 +8872,48 @@
],
"diags": []
},
"<repo>/tests/corpus/checked-callback-options.ts": {
"order": [
"<repo>/tests/corpus/checked-callback-options.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-class-descriptors.mjs": {
"order": [
"<repo>/tests/corpus/checked-class-descriptors.mjs"
],
"diags": []
},
"<repo>/tests/corpus/checked-class-factory.mjs": {
"order": [
"<repo>/tests/corpus/checked-class-factory.mjs"
],
"diags": []
},
"<repo>/tests/corpus/checked-collection-iterators.ts": {
"order": [
"<repo>/tests/corpus/checked-collection-iterators.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-collection-symbol-methods.ts": {
"order": [
"<repo>/tests/corpus/checked-collection-symbol-methods.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-increment/main.js": {
"order": [
"<repo>/tests/corpus/checked-increment/main.js"
],
"diags": []
},
"<repo>/tests/corpus/checked-inherited-fields.ts": {
"order": [
"<repo>/tests/corpus/checked-inherited-fields.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-numeric-arithmetic.js": {
"order": [
"<repo>/tests/corpus/checked-numeric-arithmetic.js"
@@ -8847,6 +8926,31 @@
],
"diags": []
},
"<repo>/tests/corpus/checked-optional-calls.mjs": {
"order": [
"<repo>/tests/corpus/checked-optional-calls.mjs"
],
"diags": []
},
"<repo>/tests/corpus/checked-promise-recovery.ts": {
"order": [
"<repo>/tests/corpus/checked-promise-recovery.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-proxy-callback/main.ts": {
"order": [
"<repo>/tests/corpus/checked-proxy-callback/operations.mjs",
"<repo>/tests/corpus/checked-proxy-callback/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-record-constructor.ts": {
"order": [
"<repo>/tests/corpus/checked-record-constructor.ts"
],
"diags": []
},
"<repo>/tests/corpus/checked-renderer-defaults.js": {
"order": [
"<repo>/tests/corpus/checked-renderer-defaults.js"
@@ -8859,6 +8963,12 @@
],
"diags": []
},
"<repo>/tests/corpus/checked-symbol-array.ts": {
"order": [
"<repo>/tests/corpus/checked-symbol-array.ts"
],
"diags": []
},
"<repo>/tests/corpus/class-accessor-returned-function.js": {
"order": [
"<repo>/tests/corpus/class-accessor-returned-function.js"
@@ -8913,12 +9023,48 @@
],
"diags": []
},
"<repo>/tests/corpus/class-curried-factory.mjs": {
"order": [
"<repo>/tests/corpus/class-curried-factory.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-data-descriptors.js": {
"order": [
"<repo>/tests/corpus/class-data-descriptors.js"
],
"diags": []
},
"<repo>/tests/corpus/class-factory-inherited-statics.mjs": {
"order": [
"<repo>/tests/corpus/class-factory-inherited-statics.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-factory-parameter-base.mjs": {
"order": [
"<repo>/tests/corpus/class-factory-parameter-base.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-factory-record-base.mjs": {
"order": [
"<repo>/tests/corpus/class-factory-record-base.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-factory-recursive-statics.mjs": {
"order": [
"<repo>/tests/corpus/class-factory-recursive-statics.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-factory-static-members.mjs": {
"order": [
"<repo>/tests/corpus/class-factory-static-members.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-function-base/main.js": {
"order": [
"<repo>/tests/corpus/class-function-base/base.js",
@@ -8938,6 +9084,18 @@
],
"diags": []
},
"<repo>/tests/corpus/class-json-checked.ts": {
"order": [
"<repo>/tests/corpus/class-json-checked.ts"
],
"diags": []
},
"<repo>/tests/corpus/class-local-function-base.mjs": {
"order": [
"<repo>/tests/corpus/class-local-function-base.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-property-call-values.mjs": {
"order": [
"<repo>/tests/corpus/class-property-call-values.mjs"
@@ -8950,12 +9108,42 @@
],
"diags": []
},
"<repo>/tests/corpus/class-prototype-descriptor-copy.mjs": {
"order": [
"<repo>/tests/corpus/class-prototype-descriptor-copy.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-specialized-computed-base.mjs": {
"order": [
"<repo>/tests/corpus/class-specialized-computed-base.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-static-data.js": {
"order": [
"<repo>/tests/corpus/class-static-data.js"
],
"diags": []
},
"<repo>/tests/corpus/class-super-surplus-spread.mjs": {
"order": [
"<repo>/tests/corpus/class-super-surplus-spread.mjs"
],
"diags": []
},
"<repo>/tests/corpus/class-symbol-accessors.ts": {
"order": [
"<repo>/tests/corpus/class-symbol-accessors.ts"
],
"diags": []
},
"<repo>/tests/corpus/class-symbol-cache.mjs": {
"order": [
"<repo>/tests/corpus/class-symbol-cache.mjs"
],
"diags": []
},
"<repo>/tests/corpus/closure-nullable-union-return.ts": {
"order": [
"<repo>/tests/corpus/closure-nullable-union-return.ts"
@@ -9004,6 +9192,12 @@
],
"diags": []
},
"<repo>/tests/corpus/conditional-tuple-destructuring.ts": {
"order": [
"<repo>/tests/corpus/conditional-tuple-destructuring.ts"
],
"diags": []
},
"<repo>/tests/corpus/console-imports.cjs": {
"order": [
"<repo>/tests/corpus/console-imports.cjs"
@@ -9082,6 +9276,24 @@
],
"diags": []
},
"<repo>/tests/corpus/error-inherited-message.mjs": {
"order": [
"<repo>/tests/corpus/error-inherited-message.mjs"
],
"diags": []
},
"<repo>/tests/corpus/error-inherited-name.mjs": {
"order": [
"<repo>/tests/corpus/error-inherited-name.mjs"
],
"diags": []
},
"<repo>/tests/corpus/error-property-writes.mjs": {
"order": [
"<repo>/tests/corpus/error-property-writes.mjs"
],
"diags": []
},
"<repo>/tests/corpus/error-prototype-tostring.js": {
"order": [
"<repo>/tests/corpus/error-prototype-tostring.js"
@@ -9178,6 +9390,18 @@
],
"diags": []
},
"<repo>/tests/corpus/function-apply-value.mjs": {
"order": [
"<repo>/tests/corpus/function-apply-value.mjs"
],
"diags": []
},
"<repo>/tests/corpus/function-constructor-global.mjs": {
"order": [
"<repo>/tests/corpus/function-constructor-global.mjs"
],
"diags": []
},
"<repo>/tests/corpus/function-constructor-kinds/main.js": {
"order": [
"<repo>/tests/corpus/function-constructor-kinds/consumer.js",
@@ -9191,6 +9415,12 @@
],
"diags": []
},
"<repo>/tests/corpus/function-custom-to-string.mjs": {
"order": [
"<repo>/tests/corpus/function-custom-to-string.mjs"
],
"diags": []
},
"<repo>/tests/corpus/function-object-assign.js": {
"order": [
"<repo>/tests/corpus/function-object-assign.js"
@@ -9204,6 +9434,18 @@
],
"diags": []
},
"<repo>/tests/corpus/generator-checked-protocol.ts": {
"order": [
"<repo>/tests/corpus/generator-checked-protocol.ts"
],
"diags": []
},
"<repo>/tests/corpus/generator-checked-return.js": {
"order": [
"<repo>/tests/corpus/generator-checked-return.js"
],
"diags": []
},
"<repo>/tests/corpus/generator-custom-delegation/main.ts": {
"order": [
"<repo>/tests/corpus/generator-custom-delegation/iterators.js",
@@ -9211,6 +9453,12 @@
],
"diags": []
},
"<repo>/tests/corpus/generator-default-return-pattern.ts": {
"order": [
"<repo>/tests/corpus/generator-default-return-pattern.ts"
],
"diags": []
},
"<repo>/tests/corpus/generator-delegation-values.ts": {
"order": [
"<repo>/tests/corpus/generator-delegation-values.ts"
@@ -9317,6 +9565,78 @@
],
"diags": []
},
"<repo>/tests/corpus/inferred-array-assignment-values.mjs": {
"order": [
"<repo>/tests/corpus/inferred-array-assignment-values.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-class-default.mjs": {
"order": [
"<repo>/tests/corpus/inferred-class-default.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-compound-values.mjs": {
"order": [
"<repo>/tests/corpus/inferred-compound-values.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-conditional-class.mjs": {
"order": [
"<repo>/tests/corpus/inferred-conditional-class.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-error-fields.js": {
"order": [
"<repo>/tests/corpus/inferred-error-fields.js"
],
"diags": []
},
"<repo>/tests/corpus/inferred-map-patterns.mjs": {
"order": [
"<repo>/tests/corpus/inferred-map-patterns.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-method-override.js": {
"order": [
"<repo>/tests/corpus/inferred-method-override.js"
],
"diags": []
},
"<repo>/tests/corpus/inferred-partial-destructuring.mjs": {
"order": [
"<repo>/tests/corpus/inferred-partial-destructuring.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-primitive-properties.mjs": {
"order": [
"<repo>/tests/corpus/inferred-primitive-properties.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-record-return.mjs": {
"order": [
"<repo>/tests/corpus/inferred-record-return.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-recursive-record.mjs": {
"order": [
"<repo>/tests/corpus/inferred-recursive-record.mjs"
],
"diags": []
},
"<repo>/tests/corpus/inferred-undefined-field.mjs": {
"order": [
"<repo>/tests/corpus/inferred-undefined-field.mjs"
],
"diags": []
},
"<repo>/tests/corpus/instanceof-class-record-union.ts": {
"order": [
"<repo>/tests/corpus/instanceof-class-record-union.ts"
@@ -9524,6 +9844,12 @@
],
"diags": []
},
"<repo>/tests/corpus/json-runtime-options.mjs": {
"order": [
"<repo>/tests/corpus/json-runtime-options.mjs"
],
"diags": []
},
"<repo>/tests/corpus/json-stringify-value.js": {
"order": [
"<repo>/tests/corpus/json-stringify-value.js"
@@ -9851,6 +10177,12 @@
],
"diags": []
},
"<repo>/tests/corpus/nullable-object-extension.mjs": {
"order": [
"<repo>/tests/corpus/nullable-object-extension.mjs"
],
"diags": []
},
"<repo>/tests/corpus/nullish-assignment-values.ts": {
"order": [
"<repo>/tests/corpus/nullish-assignment-values.ts"
@@ -9881,6 +10213,12 @@
],
"diags": []
},
"<repo>/tests/corpus/object-prototype-identity.mjs": {
"order": [
"<repo>/tests/corpus/object-prototype-identity.mjs"
],
"diags": []
},
"<repo>/tests/corpus/object-reexport/main.js": {
"order": [
"<repo>/tests/corpus/object-reexport/core.js",
@@ -9902,6 +10240,12 @@
],
"diags": []
},
"<repo>/tests/corpus/optional-callback-arity.ts": {
"order": [
"<repo>/tests/corpus/optional-callback-arity.ts"
],
"diags": []
},
"<repo>/tests/corpus/optional-native-callables.js": {
"order": [
"<repo>/tests/corpus/optional-native-callables.js"
@@ -10004,6 +10348,18 @@
],
"diags": []
},
"<repo>/tests/corpus/promise-checked-rejections.ts": {
"order": [
"<repo>/tests/corpus/promise-checked-rejections.ts"
],
"diags": []
},
"<repo>/tests/corpus/promise-resolve-void.ts": {
"order": [
"<repo>/tests/corpus/promise-resolve-void.ts"
],
"diags": []
},
"<repo>/tests/corpus/prototype-method-overrides.mjs": {
"order": [
"<repo>/tests/corpus/prototype-method-overrides.mjs"
@@ -10028,6 +10384,24 @@
],
"diags": []
},
"<repo>/tests/corpus/reflect-own-keys.mjs": {
"order": [
"<repo>/tests/corpus/reflect-own-keys.mjs"
],
"diags": []
},
"<repo>/tests/corpus/reflect-record-union-keys.mjs": {
"order": [
"<repo>/tests/corpus/reflect-record-union-keys.mjs"
],
"diags": []
},
"<repo>/tests/corpus/reflect-typed-record-keys.ts": {
"order": [
"<repo>/tests/corpus/reflect-typed-record-keys.ts"
],
"diags": []
},
"<repo>/tests/corpus/regex-checked-casts.ts": {
"order": [
"<repo>/tests/corpus/regex-checked-casts.ts"
@@ -10173,6 +10547,12 @@
],
"diags": []
},
"<repo>/tests/corpus/string-split-checked-separator.mjs": {
"order": [
"<repo>/tests/corpus/string-split-checked-separator.mjs"
],
"diags": []
},
"<repo>/tests/corpus/structural-class-properties.ts": {
"order": [
"<repo>/tests/corpus/structural-class-properties.ts"
@@ -10310,6 +10690,13 @@
],
"diags": []
},
"<repo>/tests/corpus/typed-collection-views/main.ts": {
"order": [
"<repo>/tests/corpus/typed-collection-views/use.mjs",
"<repo>/tests/corpus/typed-collection-views/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/typed-prototype-methods.ts": {
"order": [
"<repo>/tests/corpus/typed-prototype-methods.ts"
@@ -10436,6 +10823,12 @@
],
"diags": []
},
"<repo>/tests/corpus/weak-native-class-identity.ts": {
"order": [
"<repo>/tests/corpus/weak-native-class-identity.ts"
],
"diags": []
},
"<repo>/tests/corpus/weak-set-native.js": {
"order": [
"<repo>/tests/corpus/weak-set-native.js"
+1
View File
@@ -134,6 +134,7 @@ void scr_assert_fail_msg(ScrStr *message) {
scr_str_release(message); /* scr_error_new retained its copy */
scr_str_release(e->name);
e->name = scr_str_new("AssertionError", 14);
e->name_present = true;
scr_error_set_code(e, "ERR_ASSERTION");
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
}
+6
View File
@@ -3298,6 +3298,7 @@ static void scr_gen_exc_move(ScrExcCell *dst, ScrExcCell *src) {
struct ScrGen {
size_t rc;
ScrDyn *receiver;
ScrFiber *fiber; /* NULL once torn down (done, or unstarted release) */
int state;
ScrGenSlot out; /* yielded value / completion value */
@@ -3322,6 +3323,7 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa
ScrGen *g = calloc(1, sizeof *g);
if (!g) scr_oom();
g->rc = 1;
g->receiver = scr_dyn_this_get();
g->state = SCR_GEN_UNSTARTED;
g->drop_args = drop_args;
g->is_async = settle_async != NULL;
@@ -3368,6 +3370,7 @@ ScrGen *scr_gen_retain(ScrGen *g) {
void scr_gen_release(ScrGen *g) {
if (!g || --g->rc != 0) return;
scr_dyn_release(g->receiver);
scr_gen_slot_reset(&g->out);
scr_gen_slot_reset(&g->in);
scr_gen_slot_reset(&g->ret);
@@ -3399,6 +3402,9 @@ void scr_gen_release_v(void *g) { scr_gen_release((ScrGen *)g); }
bool scr_gen_done(ScrGen *g) { return g->state == SCR_GEN_DONE; }
ScrGen *scr_gen_of_fiber(ScrFiber *f) { return f->gen; }
ScrDyn *scr_gen_receiver(void) {
return scr_dyn_retain(scr_current && scr_current->gen ? scr_current->gen->receiver : scr_dyn_undefined());
}
/* Slot setters (release the previous occupant; payloads MOVE in). */
static void scr_gen_slot_f64(ScrGenSlot *s, double v) {
+43
View File
@@ -496,6 +496,47 @@ ScrDyn *scr_dyn_promise_then(ScrPromise *src, ScrDyn *onf, ScrDyn *onr, ScrDyn *
scr_promise_release(waiter); /* the entry never rejects; nobody awaits it */
return boxed;
}
typedef struct {
ScrPromise *destination;
ScrDyn *value;
} ScrDynResolvePack;
static void scr_dyn_resolve_entry(ScrFiber *self, void *opaque) {
(void)self;
ScrDynResolvePack *pack = opaque;
/* Promise resolution schedules a job before attaching its reaction. */
scr_await_hop();
ScrDyn *value = scr_await_dyn(pack->value->v.promise);
if (scr_exc_pending()) scr_promise_reject_pending(pack->destination);
else scr_promise_resolve_dyn(pack->destination, value);
scr_dyn_release(pack->value);
scr_promise_release(pack->destination);
free(pack);
}
/* The destination is borrowed; value moves in. An async function may
* return a native promise hidden behind any/unknown without an explicit
* await, so its completion must use resolution rather than fulfillment. */
void scr_promise_resolve_dyn(ScrPromise *destination, ScrDyn *value) {
if (value->kind != SCR_DYN_PROMISE) {
scr_promise_fulfill_ref(destination, value, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
return;
}
if (value->v.promise == destination) {
static const char message[] = "Chaining cycle detected for promise #<Promise>";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
scr_promise_reject_pending(destination);
scr_dyn_release(value);
return;
}
ScrDynResolvePack *pack = malloc(sizeof *pack);
if (!pack) scr_ad_oom();
pack->destination = scr_promise_retain(destination);
pack->value = value;
ScrPromise *waiter = scr_async_spawn(scr_dyn_resolve_entry, pack);
scr_promise_release(waiter);
}
/* `await v` where v is a CHECKED-DYNAMIC value: a dyn promise adopts
* (the boxed promise awaits — rejections re-throw); every other kind is
* JS's await-of-a-non-thenable — one microtask hop, the value itself
@@ -597,6 +638,7 @@ void scr_emit_warning(const char *name, const char *code, ScrStr *message) {
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
scr_str_release(e->name);
e->name = scr_str_new(name ? name : "Warning", strlen(name ? name : "Warning"));
e->name_present = true;
if (code) e->code = scr_str_new(code, strlen(code));
ScrDyn *w = scr_dyn_from_error(e);
scr_error_release(e);
@@ -670,6 +712,7 @@ void scr_process_emit_warning(ScrDyn *args) {
ScrError *e = scr_error_new(SCR_ERR_ERROR, (ScrStr *)warning->v.str);
scr_str_release(e->name);
e->name = type ? scr_str_retain((ScrStr *)type) : scr_str_new("Warning", 7);
e->name_present = true;
if (code) e->code = scr_str_retain((ScrStr *)code);
ScrDyn *w = scr_dyn_from_error(e);
scr_error_release(e);
+120 -26
View File
@@ -648,6 +648,14 @@ static bool dyn_arr_flatten(ScrDyn *out, const ScrDyn *source, double depth, siz
return false;
}
const ScrDyn *item = source->v.arr.items[i];
if (depth > 0 && item->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(item)) {
ScrDyn *view = scr_dyn_typed_ref_materialize(item);
if (!view || scr_exc_pending()) { scr_dyn_release(view); return false; }
bool ok = dyn_arr_flatten(out, view, depth - 1, visits);
scr_dyn_release(view);
if (!ok) return false;
continue;
}
if (depth > 0 && item->kind == SCR_DYN_ARR) {
if (!dyn_arr_flatten(out, item, depth - 1, visits)) return false;
} else {
@@ -788,32 +796,8 @@ static ScrDyn *scr_dyn_invoke_impl(
if (recv->kind == SCR_DYN_FUNC) {
if (dyn_name_is(method, "apply")) {
/* fn.apply(thisArg, argsArray) — thisArg binds the ambient receiver
* for the call window (the mustCall wrapper's `fn.apply(this,
* arguments)` forwards whatever receiver the wrapper ran under). */
const ScrDyn *thisv = argc >= 1 ? args[0] : scr_dyn_undefined();
ScrDyn *list = argc >= 2 ? args[1] : NULL;
if (list == NULL || list->kind == SCR_DYN_UNDEF || list->kind == SCR_DYN_NULL) {
scr_dyn_this_push_dyn(thisv);
ScrDyn *r = scr_dyn_call(recv, NULL, 0, what);
scr_dyn_this_pop();
return r;
}
ScrDyn *view = list->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(list)
? scr_dyn_typed_ref_materialize(list) : NULL;
if (scr_exc_pending()) { scr_dyn_release(view); return NULL; }
if (view) list = view;
if (list->kind != SCR_DYN_ARR) {
scr_throw_error_msg(SCR_ERR_TYPE, "CreateListFromArrayLike called on non-object",
strlen("CreateListFromArrayLike called on non-object"));
scr_dyn_release(view);
return NULL;
}
scr_dyn_this_push_dyn(thisv);
ScrDyn *r = scr_dyn_call(recv, list->v.arr.items, list->v.arr.len, what);
scr_dyn_this_pop();
scr_dyn_release(view);
return r;
return scr_dyn_apply_array_like(recv, argc ? args[0] : scr_dyn_undefined(),
argc > 1 ? args[1] : scr_dyn_undefined(), what);
}
if (dyn_name_is(method, "call")) {
scr_dyn_this_push_dyn(argc >= 1 ? args[0] : scr_dyn_undefined());
@@ -823,6 +807,15 @@ static ScrDyn *scr_dyn_invoke_impl(
}
if (dyn_name_is(method, "bind")) return scr_dyn_bind(recv, args, argc);
if (dyn_name_is(method, "toString")) {
ScrDyn *member = scr_dyn_fn_get(recv, method, strlen(method));
if (member && member->kind != SCR_DYN_UNDEF) {
scr_dyn_this_push_dyn(recv);
ScrDyn *result = scr_dyn_call(member, args, argc, what);
scr_dyn_this_pop();
scr_dyn_release(member);
return result;
}
scr_dyn_release(member);
dyn_throw_unsupported("Function", method);
return NULL;
}
@@ -1487,6 +1480,107 @@ static ScrDyn *dyn_array_method_value_call(ScrClosure *closure, ScrDyn *const *a
return result;
}
static SCR_TL ScrDyn *dyn_builtin_methods;
static void dyn_builtin_methods_cleanup(void) {
scr_dyn_release(dyn_builtin_methods);
dyn_builtin_methods = NULL;
}
static ScrDyn *dyn_builtin_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
ScrStr *prototype = scr_box_get_ref(closure->caps[0]);
ScrStr *method = scr_box_get_ref(closure->caps[1]);
ScrDyn *receiver = scr_dyn_this_get();
ScrDyn *result = NULL;
if (!strcmp(prototype->data, "Object")) {
ScrDyn *key = argc ? args[0] : scr_dyn_undefined();
if (!strcmp(method->data, "toString")) {
ScrStr *tag = scr_dyn_object_tag(receiver);
if (tag) { result = scr_dyn_new_str(tag); scr_str_release(tag); }
} else if (!strcmp(method->data, "hasOwnProperty")) {
bool own = scr_dyn_has_own_computed(receiver, key);
if (!scr_exc_pending()) result = scr_dyn_new_bool(own);
} else if (!strcmp(method->data, "propertyIsEnumerable")) {
bool enumerable = scr_dyn_property_is_enumerable_computed(receiver, key);
if (!scr_exc_pending()) result = scr_dyn_new_bool(enumerable);
} else if (receiver->kind == SCR_DYN_NULL || receiver->kind == SCR_DYN_UNDEF) {
static const char message[] = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
} else if (receiver->kind == SCR_DYN_OBJ || receiver->kind == SCR_DYN_ARR ||
receiver->kind == SCR_DYN_FUNC || receiver->kind == SCR_DYN_HANDLE ||
receiver->kind == SCR_DYN_TYPED_REF || receiver->kind == SCR_DYN_PROXY) result = scr_dyn_retain(receiver);
else dyn_throw_unsupported("Object", method->data);
} else {
bool compatible = false;
if (!strcmp(prototype->data, "String")) compatible = receiver->kind != SCR_DYN_UNDEF && receiver->kind != SCR_DYN_NULL;
else if (!strcmp(prototype->data, "Number")) compatible = receiver->kind == SCR_DYN_NUM;
else if (receiver->kind == SCR_DYN_HANDLE) {
const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(receiver);
compatible = !strcmp(ops->cls, prototype->data);
}
if (!compatible) {
static const char message[] = "Builtin prototype method called on incompatible receiver";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
} else if (!strcmp(method->data, "toString") && receiver->kind == SCR_DYN_NUM) {
ScrStr *value = scr_dyn_to_string_argument(receiver, argc ? args[0] : scr_dyn_undefined(), method);
if (value) { result = scr_dyn_new_str(value); scr_str_release(value); }
} else if (!strcmp(prototype->data, "String")) {
bool strict = !strcmp(method->data, "toString") || !strcmp(method->data, "valueOf");
if (strict && receiver->kind != SCR_DYN_STR) {
static const char message[] = "String.prototype method requires a string receiver";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
} else if (strict) result = scr_dyn_retain(receiver);
else {
ScrStr *text = scr_dyn_string_coerce_js(receiver);
if (text) {
ScrDyn *string = scr_dyn_new_str(text);
scr_str_release(text);
if (!strcmp(method->data, "slice")) result = scr_dyn_invoke(string, method->data, args, argc, method->data);
else dyn_throw_unsupported("String", method->data);
scr_dyn_release(string);
}
}
} else result = scr_dyn_invoke(receiver, method->data, args, argc, method->data);
}
scr_dyn_release(receiver);
scr_str_release(prototype);
scr_str_release(method);
return result;
}
ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method) {
if (!dyn_builtin_methods) {
dyn_builtin_methods = scr_dyn_new_obj_null_proto();
scr_atexit(dyn_builtin_methods_cleanup);
}
ScrJsonBuf buffer;
scr_jb_init(&buffer);
scr_jb_puts(&buffer, prototype->data);
scr_jb_putc(&buffer, '.');
scr_jb_puts(&buffer, method->data);
ScrStr *key = scr_jb_finish(&buffer);
ScrDyn *value = scr_dyn_obj_get(dyn_builtin_methods, key->data, key->len);
if (!value) {
ScrClosure *closure = scr_closure_new(NULL, 2);
closure->caps[0] = scr_box_new(SCR_BOX_STR);
closure->caps[1] = scr_box_new(SCR_BOX_STR);
scr_box_set_ref(closure->caps[0], scr_str_retain((ScrStr *)prototype));
scr_box_set_ref(closure->caps[1], scr_str_retain((ScrStr *)method));
size_t arity = (!strcmp(method->data, "set") || !strcmp(method->data, "substring") ||
!strcmp(method->data, "slice") ||
!strcmp(method->data, "replace") || !strcmp(method->data, "split")) ? 2 :
(!strcmp(method->data, "get") || !strcmp(method->data, "add") || !strcmp(method->data, "has") ||
!strcmp(method->data, "delete") || !strcmp(method->data, "forEach") || !strcmp(method->data, "startsWith") ||
!strcmp(method->data, "endsWith") || !strcmp(method->data, "padStart") || !strcmp(method->data, "toJSON") ||
!strcmp(method->data, "charCodeAt") || !strcmp(method->data, "hasOwnProperty") ||
!strcmp(method->data, "propertyIsEnumerable") || (!strcmp(prototype->data, "Number") && !strcmp(method->data, "toString"))) ? 1 : 0;
value = scr_dyn_new_func(closure, dyn_builtin_method_call, arity, "native:prototype", method->data);
scr_dyn_obj_set(dyn_builtin_methods, key->data, key->len, value);
}
scr_str_release(key);
return scr_dyn_retain(value);
}
ScrDyn *scr_dyn_array_prototype(void) {
static SCR_TL bool initialized;
ScrDyn *prototype = scr_dyn_array_prototype_base();
+52 -5
View File
@@ -135,6 +135,7 @@ void scr_error_init(void *obj, int kind, ScrStr *message) {
const char *n = scr_error_names[kind];
e->name = scr_str_new(n, strlen(n));
e->message = message ? scr_str_retain(message) : scr_str_new("", 0);
e->message_present = message != NULL;
e->stack_frames = scr_stack_capture();
}
@@ -172,9 +173,28 @@ ScrError *scr_error_new(int kind, ScrStr *message) {
return e;
}
ScrStr *scr_error_to_string(ScrError *e) {
/* ECMA-262 Error.prototype.toString over the two fields (no prototype
* chain exists: constructors always initialize both) — except Node's
static SCR_TL ScrStr *(*scr_error_message_reader)(ScrError *);
static SCR_TL ScrStr *(*scr_error_name_reader)(ScrError *);
void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)) {
scr_error_message_reader = reader;
}
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)) {
scr_error_name_reader = reader;
}
ScrStr *scr_error_default_name(const ScrError *e) {
for (int i = 1; i < SCR_ERR_COUNT; i++) {
if (i == SCR_ERR_DOMEX) continue;
if (scr_error_vts[i].pre <= e->vt->pre && e->vt->pre <= scr_error_vts[i].post)
return scr_str_new(scr_error_names[i], strlen(scr_error_names[i]));
}
return scr_str_new("Error", 5);
}
static ScrStr *scr_error_format(ScrError *e, ScrStr *name, ScrStr *message) {
/* ECMA-262 Error.prototype.toString over the resolved properties — except Node's
* OWN error classes: AssertionError's toString and the NodeError
* family's construction-time name both render "name [code]: message".
* Every ERR_*-coded error this runtime mints IS one of those classes
@@ -182,8 +202,6 @@ ScrStr *scr_error_to_string(ScrError *e) {
* the code-prefix test is exact for runtime-thrown errors; a user
* assigning an ERR_* code onto a plain Error would bracket here where
* Node would not (documented — user code owns that spelling). */
ScrStr *name = e->name;
ScrStr *message = e->message;
bool assertion = name && e->code && e->code->len >= 4 &&
memcmp(e->code->data, "ERR_", 4) == 0;
if (!name || name->len == 0) {
@@ -214,9 +232,36 @@ ScrStr *scr_error_to_string(ScrError *e) {
return out;
}
ScrStr *scr_error_to_string(ScrError *e) {
// Diagnostic formatting can run with the original error still pending.
// Accessors execute with a clear cell, and cannot replace that error.
ScrCaught *pending = scr_exc_pending() ? scr_exc_take() : NULL;
ScrStr *name = !e->name_present && scr_error_name_reader
? scr_error_name_reader(scr_error_retain(e)) : scr_str_retain(e->name);
ScrStr *message = scr_exc_pending() ? NULL : !e->message_present && scr_error_message_reader
? scr_error_message_reader(scr_error_retain(e)) : scr_str_retain(e->message);
if (pending) {
if (scr_exc_pending()) {
scr_exc_clear();
scr_str_release(message);
message = scr_str_retain(e->message);
scr_str_release(name);
name = scr_str_retain(e->name);
}
scr_rethrow(pending);
scr_caught_release(pending);
}
if (!name || !message) { scr_str_release(name); scr_str_release(message); return NULL; }
ScrStr *result = scr_error_format(e, name, message);
scr_str_release(name);
scr_str_release(message);
return result;
}
ScrStr *scr_error_stack(ScrError *e) {
if (!e->stack) {
ScrStr *heading = scr_error_to_string(e);
if (!heading) return NULL;
e->stack = e->stack_frames ? scr_str_concat(heading, e->stack_frames) : scr_str_retain(heading);
scr_str_release(heading);
}
@@ -367,6 +412,8 @@ void scr_throw_error_named(ScrStr *name, ScrStr *message) {
ScrError *e = scr_error_alloc(kind);
e->name = name; /* ownership moves in (engine names may be custom) */
e->message = message; /* ownership moves in */
e->name_present = kind == SCR_ERR_ERROR && !(name->len == 5 && memcmp(name->data, "Error", 5) == 0);
e->message_present = true;
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
scr_error_traced ? &scr_error_trace : NULL);
}
+21 -5
View File
@@ -139,11 +139,24 @@ void scr_exc_clear(void) { scr_exc_reset(); }
/* ── catch bindings (ScrCaught — contract in scr_runtime.h) ──────────── */
ScrCaught *scr_exc_take(void) {
ScrCaught *c = calloc(1, sizeof *c);
if (!c) {
scr_trap("scriptc: out of memory\n");
void scr_caught_trace_v(void *object, ScrTraceVisit visit, void *ctx) {
ScrCaught *c = object;
if (c->payload && c->trace_fn) visit(c->payload, ctx);
}
static void scr_caught_gcfree(void *object) {
ScrCaught *c = object;
if (c->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)c->payload);
} else if (c->payload && c->release_fn && !c->trace_fn) {
c->release_fn(c->payload);
}
scr_obj_free_note();
scr_cyc_free(c);
}
ScrCaught *scr_exc_take(void) {
ScrCaught *c = scr_cyc_alloc(sizeof *c, scr_caught_trace_v, scr_caught_gcfree);
c->rc = 1;
c->kind = scr_exc_kind;
c->f64 = scr_exc_f64;
@@ -169,6 +182,7 @@ ScrCaught *scr_caught_retain(ScrCaught *c) {
void scr_caught_release(ScrCaught *c) {
if (!c) return;
if (--c->rc == 0) {
scr_cyc_on_dead(c);
if (c->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)c->payload);
} else if (c->kind == SCR_EXC_REF || c->kind == SCR_EXC_OBJ ||
@@ -176,7 +190,9 @@ void scr_caught_release(ScrCaught *c) {
c->release_fn(c->payload);
}
scr_obj_free_note();
free(c);
scr_cyc_free(c);
} else {
scr_cyc_on_release(c);
}
}
+26 -13
View File
@@ -1356,19 +1356,7 @@ static bool isl_dynjs_assign(ScrJsval *cell, const ScrDyn *src) {
static ScrStr *isl_dynjs_to_json(ScrJsval *cell) { return scr_jsval_to_json(cell); }
static ScrDyn *isl_dynjs_iter_drain(ScrJsval *cell, bool spread, const ScrStr *spell) {
isl_entry();
JSValue m = JS_NewInt32(isl_ctx, spread ? 1 : 0);
JSValue s = spell && spell->len > 0
? JS_NewStringLen(isl_ctx, spell->data, spell->len)
: JS_UNDEFINED;
JSValue argv[3] = {cell->v, m, s};
JSValue r = JS_Call(isl_ctx, isl_helpers[ISL_H_ITERDRAIN], JS_UNDEFINED, 3, argv);
JS_FreeValue(isl_ctx, s);
if (JS_IsException(r)) {
isl_bridge_exception();
return NULL;
}
static ScrDyn *isl_dynjs_unpack_array(JSValue r) {
/* The drained engine array unpacks into a fresh dyn array — elements
* wrap back scalar-normalized (composites stay engine values by
* reference), exactly the obj_walk unpack. */
@@ -1386,6 +1374,22 @@ static ScrDyn *isl_dynjs_iter_drain(ScrJsval *cell, bool spread, const ScrStr *s
return out;
}
static ScrDyn *isl_dynjs_iter_drain(ScrJsval *cell, bool spread, const ScrStr *spell) {
isl_entry();
JSValue m = JS_NewInt32(isl_ctx, spread ? 1 : 0);
JSValue s = spell && spell->len > 0
? JS_NewStringLen(isl_ctx, spell->data, spell->len)
: JS_UNDEFINED;
JSValue argv[3] = {cell->v, m, s};
JSValue r = JS_Call(isl_ctx, isl_helpers[ISL_H_ITERDRAIN], JS_UNDEFINED, 3, argv);
JS_FreeValue(isl_ctx, s);
if (JS_IsException(r)) {
isl_bridge_exception();
return NULL;
}
return isl_dynjs_unpack_array(r);
}
static ScrDyn *isl_dynjs_iterator(ScrJsval *cell, const ScrStr *spell, bool array_from) {
isl_entry();
JSValue s = spell && spell->len ? JS_NewStringLen(isl_ctx, spell->data, spell->len) : JS_UNDEFINED;
@@ -1398,6 +1402,14 @@ static ScrDyn *isl_dynjs_iterator(ScrJsval *cell, const ScrStr *spell, bool arra
return out;
}
static ScrDyn *isl_dynjs_iter_n(ScrJsval *cell, double count) {
ScrJsval *prefix = scr_jsval_iter_n(cell, count);
if (!prefix) return NULL;
ScrDyn *out = isl_dynjs_unpack_array(JS_DupValue(isl_ctx, prefix->v));
scr_jsval_release(prefix);
return out;
}
static const ScrDynJsvalOps isl_dynjs_ops = {
isl_dynjs_release,
isl_dynjs_typeof,
@@ -1417,6 +1429,7 @@ static const ScrDynJsvalOps isl_dynjs_ops = {
isl_dynjs_to_json,
isl_dynjs_iter_drain,
isl_dynjs_iterator,
isl_dynjs_iter_n,
};
ScrDyn *scr_dyn_from_jsval(ScrJsval *cell) {
File diff suppressed because it is too large Load Diff
+47
View File
@@ -256,6 +256,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) {
* an identity-key map may trace keys, values, or both independently. */
static void scr_map_gcfree(void *o) {
ScrMap *m = (ScrMap *)o;
scr_weak_dispose(m);
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].live) continue;
if (!m->key_trace) scr_map_release_key(m, m->entries[e].key);
@@ -314,6 +315,7 @@ ScrMap *scr_map_retain(ScrMap *m) {
void scr_map_release(ScrMap *m) {
if (!m || m->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--m->rc == 0) {
scr_weak_dispose(m);
if (m->key_trace || m->val_trace) scr_cyc_on_dead(m);
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].live) continue;
@@ -602,6 +604,51 @@ bool scr_map_iter_val_bool(const ScrMap *m, double i) {
return scr_map_iter_at(m, i)->val != 0;
}
void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops) {
map->dyn_ops = ops;
}
ScrDyn *scr_map_dyn_key(const ScrMap *map, double index) {
uint64_t slot = scr_map_iter_at(map, index)->key;
return map->dyn_ops ? map->dyn_ops->key_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot));
}
ScrDyn *scr_map_dyn_value(const ScrMap *map, double index) {
uint64_t slot = scr_map_iter_at(map, index)->val;
return map->dyn_ops ? map->dyn_ops->val_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot));
}
static size_t scr_map_dyn_find(ScrMap *map, const ScrDyn *key) {
if (map->dyn_ops && !map->dyn_ops->key_matches(key)) return SCR_MAP_EMPTY;
uint64_t slot = map->dyn_ops ? map->dyn_ops->key_unbox(key) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)key));
if (scr_exc_pending()) return SCR_MAP_EMPTY;
if (map->key_kind == SCR_MAP_KEY_F64) slot = scr_map_f64_bits(scr_map_slot_to_f64(slot));
size_t entry = scr_map_find(map, scr_map_hash_key(map, slot), slot);
scr_map_release_key(map, slot);
return entry;
}
ScrDyn *scr_map_dyn_get(ScrMap *map, const ScrDyn *key) {
size_t entry = scr_map_dyn_find(map, key);
if (scr_exc_pending()) return NULL;
return entry == SCR_MAP_EMPTY ? scr_dyn_retain(scr_dyn_undefined()) : scr_map_dyn_value(map, (double)entry);
}
bool scr_map_dyn_has(ScrMap *map, const ScrDyn *key, bool remove) {
size_t entry = scr_map_dyn_find(map, key);
return remove ? scr_map_delete_found(map, entry) : entry != SCR_MAP_EMPTY;
}
void scr_map_dyn_set(ScrMap *map, const ScrDyn *key, const ScrDyn *value, bool set) {
uint64_t key_slot = map->dyn_ops ? map->dyn_ops->key_unbox(key) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)key));
if (scr_exc_pending()) return;
uint64_t value_slot = set ? 0 : map->dyn_ops ? map->dyn_ops->val_unbox(value) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)value));
if (scr_exc_pending()) { scr_map_release_key(map, key_slot); return; }
if (map->key_kind == SCR_MAP_KEY_F64) key_slot = scr_map_f64_bits(scr_map_slot_to_f64(key_slot));
scr_map_set(map, scr_map_hash_key(map, key_slot), key_slot, value_slot);
scr_map_release_key(map, key_slot);
}
void *scr_map_iter_val_ref(const ScrMap *m, double i) {
return m->val_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->val)); /* +1 */
}
+11
View File
@@ -6,6 +6,7 @@ void scr_classobj_trace_v(void *object, ScrTraceVisit visit, void *ctx) {
ScrClassObj *c = object;
for (size_t i = 0; i < c->ncaps; i++) visit(c->caps[i], ctx);
visit(c->prototype_data, ctx);
visit(c->static_data, ctx);
}
static void scr_classobj_gcfree(void *object) {
@@ -26,6 +27,7 @@ ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) {
c->ncaps = ncaps;
c->length = template->length;
c->prototype_data = NULL;
c->static_data = NULL;
scr_obj_alloc_note();
return c;
}
@@ -36,6 +38,7 @@ void scr_classobj_release(ScrClassObj *c) {
scr_cyc_on_dead(c);
for (size_t i = 0; i < c->ncaps; i++) scr_box_release(c->caps[i]);
scr_box_release(c->prototype_data);
scr_box_release(c->static_data);
scr_classobj_gcfree(c);
} else {
scr_cyc_on_release(c);
@@ -45,6 +48,14 @@ void scr_classobj_release(ScrClassObj *c) {
void *scr_classobj_retain_v(void *c) {
return scr_classobj_retain((ScrClassObj *)c);
}
void scr_classobj_clear_properties(ScrClassObj *c) {
ScrBox *prototype = c->prototype_data, *statics = c->static_data;
c->prototype_data = NULL;
c->static_data = NULL;
scr_box_release(prototype);
scr_box_release(statics);
}
void scr_classobj_release_v(void *c) { scr_classobj_release((ScrClassObj *)c); }
ScrStr *scr_classobj_name(ScrClassObj *c) {
+53 -4
View File
@@ -472,6 +472,7 @@ typedef struct ScrClassObj {
size_t ncaps;
size_t length;
struct ScrBox *prototype_data;
struct ScrBox *static_data;
struct ScrBox *caps[];
} ScrClassObj;
@@ -483,6 +484,7 @@ static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) {
return c;
}
void scr_classobj_release(ScrClassObj *c);
void scr_classobj_clear_properties(ScrClassObj *c);
ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps);
void scr_classobj_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* void*-signature RC adapters (container slots) — scr_object.c. */
@@ -492,6 +494,7 @@ void scr_classobj_release_v(void *c);
ScrStr *scr_classobj_name(ScrClassObj *c);
struct ScrDyn *scr_dyn_class_prototype(struct ScrDyn *value, struct ScrDyn *base);
struct ScrDyn *scr_dyn_class_base_prototype(struct ScrDyn *constructor);
struct ScrDyn *scr_dyn_class_inherit(struct ScrDyn *constructor, struct ScrDyn *base);
void scr_dyn_class_super(struct ScrDyn *constructor, struct ScrDyn *receiver, struct ScrDyn *arguments);
struct ScrDyn *scr_dyn_assign_prototype(struct ScrDyn *target, struct ScrDyn *sources, struct ScrDyn *protected_keys);
/* The keyed-write miss on a fixed-shape record: throws the catchable
@@ -531,6 +534,10 @@ typedef struct ScrError {
* embed the slot and release it NULL-guarded. */
struct ScrDyn *error_cause; /* NULL = absent; dyn undefined = present */
bool cause_enumerable; /* assignment creates an enumerable own property */
bool message_present; /* super() leaves the inherited message property visible */
bool message_enumerable; /* assignment creates an enumerable own property */
bool name_present;
bool name_enumerable;
ScrStr *stack_frames; /* captured native source frames */
ScrStr *stack; /* lazily formatted, cached stack */
} ScrError;
@@ -564,6 +571,10 @@ typedef struct ScrDomException {
ScrStr *code; /* the Node string-code slot (stays NULL here) */
struct ScrDyn *error_cause; /* shared ScrError prefix; unused by DOMException */
bool cause_enumerable;
bool message_present;
bool message_enumerable;
bool name_present;
bool name_enumerable;
ScrStr *stack_frames;
ScrStr *stack;
double dom_code; /* the WebIDL legacy code (0 when the name is off-table) */
@@ -622,6 +633,9 @@ void scr_error_delete_cause(ScrError *e);
/* ECMA Error.prototype.toString: "", name, message, or "name: message".
* Borrows e, returns +1. */
ScrStr *scr_error_to_string(ScrError *e);
void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *));
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *));
ScrStr *scr_error_default_name(const ScrError *e);
ScrStr *scr_error_stack(ScrError *e);
ScrError *scr_error_retain(ScrError *e);
@@ -1277,6 +1291,17 @@ typedef struct {
bool live; /* false = tombstone (key/val already released) */
} ScrMapEntry;
/* Checked views use the original storage. Slot converters borrow on reads
* and return owned references on writes, just like typed Map operations. */
typedef struct {
const char *type;
struct ScrDyn *(*key_box)(uint64_t);
uint64_t (*key_unbox)(const struct ScrDyn *);
bool (*key_matches)(const struct ScrDyn *);
struct ScrDyn *(*val_box)(uint64_t);
uint64_t (*val_unbox)(const struct ScrDyn *);
} ScrMapDynOps;
typedef struct ScrMap {
size_t rc; /* SIZE_MAX = immortal (unused for maps; kept per convention) */
ScrMapKeyKind key_kind;
@@ -1297,8 +1322,16 @@ typedef struct ScrMap {
size_t nbuckets; /* power of two; >= 2 * ecap so probes terminate */
size_t *buckets; /* entry indices; SIZE_MAX = empty */
size_t iter_depth; /* > 0: an iteration is active — no compaction */
const ScrMapDynOps *dyn_ops;
} ScrMap;
void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops);
struct ScrDyn *scr_map_dyn_key(const ScrMap *map, double index);
struct ScrDyn *scr_map_dyn_value(const ScrMap *map, double index);
struct ScrDyn *scr_map_dyn_get(ScrMap *map, const struct ScrDyn *key);
bool scr_map_dyn_has(ScrMap *map, const struct ScrDyn *key, bool remove);
void scr_map_dyn_set(ScrMap *map, const struct ScrDyn *key, const struct ScrDyn *value, bool set);
/* retain/release/trace are NULL for scalar value kinds; retain/release are
* required for SCR_MAP_VAL_REF; trace is non-NULL iff the value type is
* cycle-capable (the map then allocates with the collector header). */
@@ -2212,10 +2245,8 @@ void scr_exc_clear(void);
* refcounted local, and every runtime question a catch body may ask —
* typeof-style kind tests (emitted as direct kind reads), instanceof over
* hierarchy payloads, payload extraction, rethrow — reads the box. The box
* itself carries no collector header: bindings live only in frames (the
* frontend rejects captures), so no cycle can pass through it, and its
* payload reference simply counts as an external edge — trial deletion
* restores it on scan. */
* participates in cycle collection because a closure may capture the
* binding and its payload may retain that closure. */
typedef struct ScrCaught {
size_t rc;
ScrExcKind kind; /* never SCR_EXC_NONE */
@@ -2230,6 +2261,7 @@ typedef struct ScrCaught {
ScrCaught *scr_exc_take(void); /* moves the pending cell into a fresh box (+1) */
ScrCaught *scr_caught_retain(ScrCaught *c);
void scr_caught_release(ScrCaught *c); /* NULL-tolerant */
void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* Replace the pending disposal error with a SuppressedError and consume the
* original body-error snapshot. The current exception cell must be pending. */
void scr_exc_suppress(ScrCaught *suppressed);
@@ -3618,6 +3650,7 @@ ScrDyn *scr_dyn_obj_keys(const ScrDyn *v);
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v);
/* Snapshot all own string keys of a SCR_DYN_OBJ in JS order. Returns +1. */
ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v);
ScrDyn *scr_dyn_own_keys(const ScrDyn *value);
ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value);
ScrDyn *scr_dyn_get_own_property_symbols(const ScrDyn *value);
/* Object.hasOwn over a dyn receiver: OBJ member presence, ARR index
@@ -3696,6 +3729,7 @@ ScrDyn *scr_dyn_new_arr(void);
ScrDyn *scr_dyn_new_obj(void);
ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell);
void scr_dyn_install_iterator_symbol(ScrDyn *key);
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
ScrDyn *scr_dyn_array_values_function(void);
ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *value);
ScrDyn *scr_dyn_iterator_result(ScrDyn *value);
@@ -3782,6 +3816,7 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what);
* compile-time source spelling), else the runtime kind wording. Borrows
* both; NULL with the exception pending on the throw. */
ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg);
ScrDyn *scr_dyn_jsval_iter_n(const ScrDyn *src, double count);
/* Map constructor seeds: retain array inputs for live iteration, accept
* nullish inputs as empty, and validate each entry before reading 0/1. */
ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src);
@@ -3836,6 +3871,11 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc);
* null-prototype dictionary throws "<what> is not a function" — its
* prototype chain has no toString (Node's answer). */
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what);
ScrDyn *scr_dyn_object_prototype(void);
ScrDyn *scr_dyn_function_apply(void);
ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what);
bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right);
ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method);
ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what);
/* JS String() over the dyn kind (units render "null"/"undefined" where
* scr_dyn_to_string throws) — the web globals' WebIDL ToString. +1. */
@@ -3882,6 +3922,9 @@ void scr_sc_validate_options(const ScrDyn *options);
/* An %Error as the boundary's dyn shape ({name, message[, code]}).
* Borrows; +1. */
ScrDyn *scr_dyn_from_error(const ScrError *e);
void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *));
ScrDyn *scr_error_dyn_fields(const ScrError *e);
void scr_error_commit_dyn(ScrError *e, const ScrDyn *view);
/* The reverse extraction, riding the same identity cache: a dyn error
* that came from a runtime ScrError answers THAT instance (+1;
* out-and-back crossings compare reference-equal); alien %error objects
@@ -3924,6 +3967,7 @@ ScrDyn *scr_dyn_define_props(ScrDyn *target, ScrDyn *descs);
* must be static literals (the box never frees them; name may be NULL). */
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key);
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
/* Calls a dyn value: a non-function kind throws the catchable TypeError
* "<what> is not a function" (Node's wording — `what` is the call site's
@@ -3949,6 +3993,8 @@ typedef struct ScrDynPath {
} ScrDynPath;
ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path);
ScrMap *scr_dyn_native_map_check(const ScrDyn *value, const ScrDynPath *path);
bool scr_dyn_native_collection_is(const ScrDyn *value, int map, const char *type);
ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char *type, const ScrDynPath *path);
ScrStr *scr_url_checked_to_path(const ScrDyn *value);
ScrUrl *scr_dyn_native_url_check(const ScrDyn *value, const ScrDynPath *path);
ScrRegex *scr_dyn_native_regex_check(const ScrDyn *value, const ScrDynPath *path);
@@ -4110,6 +4156,7 @@ typedef struct ScrDynJsvalOps {
* pending. */
ScrDyn *(*iter_drain)(ScrJsval *cell, bool spread, const ScrStr *spell);
ScrDyn *(*iterator)(ScrJsval *cell, const ScrStr *spell, bool array_from);
ScrDyn *(*iter_n)(ScrJsval *cell, double count);
} ScrDynJsvalOps;
/* The allocator view the gated constructor uses (installs the ops);
@@ -4167,6 +4214,7 @@ void scr_dyn_this_pop(void);
* handle whose tag has no installed ops — a unit that fires without its
* dyn half never binds). */
ScrDyn *scr_dyn_this_get(void);
ScrDyn *scr_gen_receiver(void);
/* The data-chunk encoding window (setEncoding — scr_json.c's note): the
* firing site opens/closes it around a 'data' pass; scr_dyn_new_chunk
@@ -4375,6 +4423,7 @@ void scr_promise_rethrow_top_level(ScrPromise *p);
* payload as a dyn value (+1; void fulfillments answer the undefined
* value), or NULL with the rejection re-thrown into the awaiter. */
ScrDyn *scr_await_dyn(ScrPromise *p);
void scr_promise_resolve_dyn(ScrPromise *destination, ScrDyn *value);
/* `await v` over a checked-dynamic VALUE: dyn promises adopt, everything
* else takes JS's one-hop non-thenable await and answers itself (+1). */
ScrDyn *scr_await_dyn_value(ScrDyn *v);
+5
View File
@@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
scr_dyn_install_iterator_symbol(key);
scr_dyn_release(key);
}
if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) {
ScrDyn *key = scr_dyn_new_symbol(symbol);
scr_dyn_install_async_iterator_symbol(key);
scr_dyn_release(key);
}
return symbol;
}
+1
View File
@@ -27,6 +27,7 @@ import { REMOTE_COMMAND_PENDING, sandboxCommand, sandboxStatusCommand, waitForSa
const root = fileURLToPath(new URL("../", import.meta.url));
const laneCaseShardedFiles = [
"tests/harness/effect.test.ts",
"tests/harness/differential.test.ts",
"tests/harness/llvm-differential.test.ts",
"tests/harness/npm.test.ts",
+24
View File
@@ -52,3 +52,27 @@ function viaTernary(win) {
}
viaTernary(false);
viaTernary(true);
// Public array signatures can be backed by checked storage in JS methods.
// Concatenation validates the returned array before spreading its elements.
class Lines {
/** @returns {string[]} */
values() {
return JSON.parse('["second","third"]');
}
render() {
const before = ['first'];
const after = before.concat(this.values());
console.log('concat:', before.join('|'), after.join('|'));
}
}
new Lines().render();
// Array predicates use ToBoolean on checked results, including objects.
const candidates = ['null', 'false', '0', '""', '{}'];
console.log('find:', candidates.find((value) => JSON.parse(value)));
console.log('find-index:', candidates.findIndex((value) => JSON.parse(value)));
console.log('find-last:', candidates.findLast((value) => JSON.parse(value)));
console.log('find-last-index:', candidates.findLastIndex((value) => JSON.parse(value)));
console.log('some:', candidates.some((value) => JSON.parse(value)));
console.log('every:', candidates.every((value) => JSON.parse(value)));
+19
View File
@@ -0,0 +1,19 @@
function dual(predicate, body) {
return function () {
if (predicate(arguments)) return body.apply(this, arguments);
return (self) => body(self, ...arguments);
};
}
const add = dual((args) => args.length === 2, (a, b) => a + b);
const addTwo = add(2);
console.log(add(20, 22), addTwo(40));
function nested(value) {
return () => () => arguments[0] + value;
}
console.log(nested(21)()());
function owners() {
return function (other) {
return () => arguments[0];
};
}
console.log(owners("outer")("inner")());
@@ -0,0 +1,9 @@
const generated = Array.from({ length: 2 }, (_, index) => new Date(index * 1000));
console.log(generated.length, generated[0].getTime(), generated[1].toISOString());
const flattened = [1, 2].flatMap((value) => new Date(value * 1000));
console.log(flattened.length, flattened[0].getTime(), flattened[1].getTime());
/** @type {[number]} */
const tuple = [3];
const mapped = tuple.map((value) => new Date(value * 1000));
console.log(mapped.length, mapped[0].getTime());
console.log([1, 2].map((value) => JSON.parse(value === 1 ? '"first"' : 'null')).join("|"));
+18
View File
@@ -0,0 +1,18 @@
const from = Array.from;
console.log(from.length, from.name);
console.log(from === Array.from, JSON.stringify(from("a😀b")));
console.log(JSON.stringify(from([1, 2], (value, index) => value + index)));
const receiver = { offset: 10 };
console.log(JSON.stringify(from([1, 2], function (value) { return this.offset + value; }, receiver)));
const source = { 0: "a", 1: "b", length: 2 };
console.log(JSON.stringify(from(source, (value, index) => {
if (index === 0) source[1] = "changed";
return value;
})));
let closed = 0;
function* values() {
try { yield 1; yield 2; } finally { closed++; }
}
try { from(values(), () => { throw new Error("mapper"); }); }
catch (error) { console.log(error.message, closed); }
try { from([], 1); } catch (error) { console.log(error.name); }
@@ -0,0 +1,45 @@
const each = Array.prototype.map;
console.log(each === Array.prototype.map, each.call([1, 2], (n) => n * 2).join(","));
const getTime = Date.prototype.getTime;
console.log(getTime === Date.prototype.getTime, getTime.call(new Date(42)));
const get = Map.prototype.get;
const set = Map.prototype.set;
const map = new Map();
console.log(set.call(map, "key", 42) === map, get.call(map, "key"));
const add = Set.prototype.add;
const has = Set.prototype.has;
const values = new Set();
console.log(add.call(values, "value") === values, has.call(values, "value"));
const weakGet = WeakMap.prototype.get;
const weakSet = WeakMap.prototype.set;
const weak = new WeakMap();
const key = {};
console.log(weakSet.call(weak, key, 21) === weak, weakGet.call(weak, key));
const tag = Object.prototype.toString;
const own = Object.prototype.hasOwnProperty;
console.log(tag.call([]), tag.call(null), own.call({ value: 1 }, "value"));
const slice = String.prototype.slice;
console.log(slice.call("hello", 1, 4));
try { getTime.call({}); } catch (error) { console.log(error.name); }
const now = Date.now;
console.log(typeof now(), now() > 0);
const min = Math.min;
const max = Math.max;
console.log(min(), max(), min(3, 1, 2), max(3, 1, 2));
const same = Object.is;
const names = Object.getOwnPropertyNames;
console.log(names({ a: 1 }).join(","));
const parse = Number.parseInt;
console.log(parse("42"), parse("ff", 16), parse(12.5));
const point = String.fromCodePoint;
const char = String.fromCharCode;
console.log(point.length, point(65, 0x1f600), point(), point("66"), char(65, 66), point === String.fromCodePoint);
try { point(-1); } catch (error) { console.log(error.name); }
let coerced = 0;
try { point(-1, { valueOf() { coerced++; return 65; } }); } catch (error) { console.log(error.name, coerced); }
console.log(char(0xd83d, 0xde00));
const create = Object.create;
const prototype = { value: 42 };
console.log(create(prototype).value, Object.getPrototypeOf(create(null)) === null);
const reference = {};
console.log(same === Object.is, same(NaN, NaN), same(0, -0), same(reference, reference), same({}, {}), same(), same(undefined));
+29
View File
@@ -0,0 +1,29 @@
function save(value: unknown): () => unknown {
try { throw value; }
catch (error) { return () => error; }
}
const error = new Error("original");
const saved = save(error)();
console.log(saved instanceof Error && saved === error, save(42)(), save("hello")());
function rethrow(): () => never {
try { throw error; }
catch (caught) { return () => { throw caught; }; }
}
try { rethrow()(); }
catch (caught) { if (caught instanceof Error) console.log(caught === error, caught instanceof Error); }
function nested(): () => () => string {
try { throw new TypeError("nested"); }
catch (caught) { return () => () => String(caught); }
}
console.log(nested()()());
function cycle() {
const object: { callback?: () => unknown } = {};
const value: unknown = object;
try { throw value; }
catch (caught) { object.callback = () => caught; }
console.log(object.callback!() === value);
}
cycle();
+6
View File
@@ -0,0 +1,6 @@
const values: unknown[] = [0];
function assign(index: any, value: any): any {
return values[index] = value;
}
console.log(assign(0, 42), values[0]);
console.log(assign("1", "end"), JSON.stringify(values));
+14
View File
@@ -0,0 +1,14 @@
import { flatten } from "./operations.mjs";
const inner = [1, 2];
const outer = [inner, [3]];
const record = { value: 4 };
const inputs = new Map<string, unknown>();
inputs.set("outer", outer);
inputs.set("record", record);
const zero = flatten(inputs, 0);
const one = flatten(inputs, 1);
const two = flatten(inputs, 2);
console.log(zero[0] === outer, one[0] === inner, two[3] === record);
console.log(JSON.stringify(zero), JSON.stringify(one), JSON.stringify(two));
inner.push(6);
console.log(JSON.stringify(flatten(inputs, 2)));
@@ -0,0 +1,3 @@
export function flatten(values, depth) {
return [values.get("outer"), values.get("record"), 5].flat(depth);
}
+8
View File
@@ -0,0 +1,8 @@
function execute(options: any): any {
return options.run(options.initial);
}
const answer = execute({ initial: 21, run: (value: any) => value * 2 });
console.log(answer);
const pair: [any, any] = [answer, "done"];
const [value, label] = pair;
console.log(value, label);
@@ -0,0 +1,23 @@
const key = Symbol("key");
class Base { inherited = 3; }
class Item extends Base {
value = { count: 7 };
[key] = "symbol";
/** @returns {string} */
get ignored() { throw new Error("prototype getter must not run"); }
}
function describe(value) {
const descriptors = Object.getOwnPropertyDescriptors(value);
console.log(Object.getOwnPropertyNames(value).join(","));
console.log(Object.getOwnPropertySymbols(value).length);
console.log(descriptors.value.value === value.value);
console.log(descriptors.value.writable, descriptors.value.enumerable, descriptors.value.configurable);
console.log(descriptors.inherited.value, descriptors[key].value);
}
const item = new Item();
describe(item);
function update(value) {
Object.assign(value, { [key]: "changed" });
return value[key];
}
console.log(update(item), item[key]);
+13
View File
@@ -0,0 +1,13 @@
function make(tag) {
class Failure extends Error {
_tag = tag;
}
return Failure;
}
const Left = make("Left");
const Right = make("Right");
function construct(Type, message) { return new Type(message); }
const left = construct(Left, "first");
const right = construct(Right, "second");
console.log(left._tag, left.message, right._tag, right.message);
console.log(left instanceof Left, right instanceof Right, left instanceof Right);
@@ -0,0 +1,25 @@
function check(map: any, set: any): void {
console.log(JSON.stringify(Array.from(map.values())));
console.log(JSON.stringify(Array.from(map.keys())));
console.log(JSON.stringify(Array.from(map.entries())));
const cursor = map.values();
console.log(JSON.stringify(cursor.next()));
map.delete("b");
map.set("c", 3);
console.log(JSON.stringify(cursor.next()), JSON.stringify(cursor.next()));
map.set("d", 4);
console.log(JSON.stringify(cursor.next()));
map.forEach((value: any, key: any, owner: any) => {
console.log(key, value, owner === map);
if (key === "a") owner.set("e", 5);
});
console.log(JSON.stringify(Array.from(set.values())));
console.log(JSON.stringify(Array.from(set.keys())));
console.log(JSON.stringify(Array.from(set.entries())));
const values = set.values();
console.log(JSON.stringify(values.next()));
set.clear();
set.add(3);
console.log(JSON.stringify(values.next()), JSON.stringify(values.next()));
}
check(new Map<unknown, unknown>([["a", 1], ["b", 2]]), new Set<unknown>([1, 2]));
@@ -0,0 +1,6 @@
const values = new Set([20, 22]);
const iterator = values[Symbol.iterator]();
console.log(iterator.next().value, iterator.next().value, iterator.next().done);
const entries = new Map([["answer", 42]]);
const first = entries[Symbol.iterator]().next();
console.log(first.done, first.value?.[0], first.value?.[1]);
+6
View File
@@ -0,0 +1,6 @@
const prototype: object = { _tag: "Ready", count: 42 };
const value: unknown = Object.create(prototype);
function read(value: { readonly _tag: string; readonly count: number; missing?: string }) {
console.log(value._tag, value.count, value.missing);
}
read(value as { _tag: string; count: number });
+20
View File
@@ -0,0 +1,20 @@
const events = [];
function argument() { events.push("arg"); return 2; }
function receiver(value) { events.push("receiver"); return value; }
const present = {
base: 40,
get callback() {
events.push("get");
return function(value) { events.push("call"); return this.base + value; };
}
};
function invoke(value) { return receiver(value).callback?.(argument()); }
console.log(invoke(present), events.join(","));
events.length = 0;
console.log(invoke({ callback: null }), events.join(","));
events.length = 0;
function key() { events.push("key"); return "callback"; }
function computed(value) { return receiver(value)[key()]?.(argument()); }
console.log(computed(present), events.join(","));
function optional(callback) { return callback?.(...[2, 3]); }
console.log(optional((a, b) => a + b), optional(undefined));
+12
View File
@@ -0,0 +1,12 @@
export {};
const source: unknown = Promise.resolve(42);
const recovered = source as Promise<unknown>;
console.log(recovered === (source as Promise<unknown>), await recovered);
function optional(value: unknown): Promise<unknown> | undefined {
return value as Promise<unknown> | undefined;
}
const present = optional(source);
if (present !== undefined) console.log(optional(undefined) === undefined, await present);
const rejected: unknown = Promise.reject("failure");
try { await (rejected as Promise<unknown>); }
catch (error) { console.log(error); }
@@ -0,0 +1,13 @@
import { makeTarget } from "./operations.mjs";
interface RequestValue { readonly kind: string; readonly value: number; }
function invoke(callback: any, value: any): any { return callback(value); }
const target: any = makeTarget();
const plain = new Proxy(target, {});
console.log(invoke((request: RequestValue) => request.kind + ":" + request.value * 2, plain));
const reads: string[] = [];
const trapped = new Proxy(target, { get(object: any, key: string): any {
reads.push(key);
return key === "value" ? 42 : object[key];
} });
console.log(invoke((request: RequestValue) => request.kind + ":" + request.value, trapped));
console.log(reads.join(","));
@@ -0,0 +1,3 @@
export function makeTarget() {
return Object.assign(Object.create({ kind: "number" }), { value: 21 });
}
@@ -0,0 +1,9 @@
class Item { value = 42; }
const key = Symbol.for("checked-record-constructor");
function check(value: unknown): { constructor: typeof Item; key: symbol } {
return value as { constructor: typeof Item; key: symbol };
}
const record = check({ constructor: Item, key });
console.log(new record.constructor().value, record.key === key);
try { new (check({ constructor: 3, key }).constructor)(); } catch (error) { console.log((error as Error).name); }
try { Symbol.keyFor(check({ constructor: Item, key: "wrong" }).key); } catch (error) { console.log((error as Error).name); }
+7
View File
@@ -0,0 +1,7 @@
function read(value: unknown): (string | symbol)[] {
return value as (string | symbol)[];
}
const key = Symbol.for("checked-symbol-array");
const values: (string | symbol)[] = ["name", key];
const result = read(values);
console.log(result.length, result[0] === "name", result[1] === key);
+21
View File
@@ -0,0 +1,21 @@
function Base(props) { Object.assign(this, props); }
const makeClass = ({ Base, label }) => {
const key = Symbol.for(label);
const Result = class extends Base {
constructor(props) { super(props); }
get [key]() { return label; }
};
Object.defineProperty(Result.prototype, "describe", {
value() { return label + ":" + this.value; },
configurable: true,
writable: true,
});
return Result;
};
const factory = label => () => makeClass({ Base, label });
class First extends factory("first")() {}
class Second extends factory("second")() {}
const first = new First({ value: 42 });
const second = new Second({ value: 7 });
console.log(first.describe(), second.describe());
console.log(first[Symbol.for("first")], second[Symbol.for("second")]);
@@ -0,0 +1,18 @@
function make(key) {
/** @constructor */
function Base() { this.value = key; }
Base.key = key;
Object.setPrototypeOf(Base, {
get label() { return this.key + "!"; },
*[Symbol.iterator]() { yield this.key; },
});
return Base;
}
class Left extends make("left") {}
class Right extends make("right") {}
function read(Type) {
console.log(Type.key, Type.label);
console.log(Type[Symbol.iterator]().next().value);
}
read(Left);
read(Right);
@@ -0,0 +1,17 @@
function Base(props) { Object.assign(this, props); }
function factory({ Base, label }) {
return class extends Base {
static label = label;
constructor(props) { super(props); }
};
}
const First = factory({ Base, label: "first" });
const Second = factory({ Base, label: "second" });
console.log(First.label, Second.label, new First({ value: 42 }).value);
function named(label) {
class Named extends First {}
Named.prototype.label = label;
return Named;
}
const Named = named("prototype");
console.log(new Named({ value: 7 }).label, new Named({ value: 7 }).value);
@@ -0,0 +1,28 @@
class YieldableError extends Error {}
const Failure = (function () {
const key = Symbol("args");
const named = {
BaseFailure: class extends YieldableError {
constructor(args) {
super(args.message);
Object.assign(this, args);
Object.defineProperty(this, key, { value: args });
}
},
};
return named.BaseFailure;
})();
function tagged(tag) {
const named = { TaggedFailure: class extends Failure { _tag = tag; } };
named.TaggedFailure.prototype.name = tag;
return named.TaggedFailure;
}
class Rejected extends tagged("Rejected") {}
const error = new Rejected({ message: "invalid", detail: 42 });
console.log(error.name, error.message, error.detail, error._tag, error instanceof Error);
console.log(error.toString(), Object.hasOwn(error, "name"));
error.name = "Custom";
console.log(error.name, error.toString(), Object.hasOwn(error, "name"));
console.log(Object.prototype.propertyIsEnumerable.call(error, "name"));
const other = new Rejected({ message: "next" });
console.log(other.name, other.toString(), Object.hasOwn(other, "name"));
@@ -0,0 +1,13 @@
function make(value) {
return class Value {
static value = value;
static copy(next) { return make(next); }
};
}
function transform(value) {
return class Transformed extends make(value) {
static copy(next) { return transform(next); }
};
}
class Final extends transform("start").copy("end") {}
console.log(Final.value, Final.copy("again").value);
@@ -0,0 +1,23 @@
function apply(value, operations) {
for (const operation of operations) value = operation(value);
return value;
}
function make(label) {
return class Counter {
static count = 0;
static label = label;
static empty;
static get description() { return this.label + ":" + this.count; }
static increment() { this.count += 1; return this.description; }
static pipe() { return apply(this, arguments); }
static [Symbol.iterator]() { return [label][Symbol.iterator](); }
};
}
const Left = make("left"), Right = make("right");
console.log(Left.increment(), Left.increment(), Right.increment());
console.log(Left.description, Right.description, Left.empty);
console.log(Left[Symbol.iterator]().next().value);
class Child extends make("child") {}
console.log(Child.increment(), Child.description);
console.log("label" in Child, Object.hasOwn(Child, "count"));
console.log(Child.pipe((Type) => Type.label), Left.pipe((Type) => Type.description));
+10
View File
@@ -0,0 +1,10 @@
class Result {
value: number;
constructor(value: number) { this.value = value; }
toJSON() { return { result: this.value }; }
}
function encode(value: object): void {
console.log(JSON.stringify(value));
console.log(JSON.stringify({ nested: value }));
}
encode(new Result(42));
@@ -0,0 +1,13 @@
function Base(value) { this.value = value; }
Base.category = "base";
function make(tag) {
class Derived extends Base {
tag = tag;
describe() { return this.tag + ":" + this.value; }
}
return Derived;
}
const Left = make("left");
const Right = make("right");
console.log(new Left(10).describe(), new Right(20).describe());
console.log(Left.category, Right.category, Left === Right);
@@ -0,0 +1,37 @@
class Item {
value = 4;
get doubled() { return this.value * 2; }
set doubled(value) { this.value = value / 2; }
add(amount) { this.value += amount; return this.value; }
toJSON() { return { value: this.value }; }
}
const originalAdd = Item.prototype.add;
function copy(value) {
const prototype = Object.getPrototypeOf(value);
console.log(Object.getOwnPropertyNames(prototype).join(","));
console.log(prototype.constructor === Item);
const descriptors = Object.getOwnPropertyDescriptors(prototype);
console.log(descriptors.add.enumerable, descriptors.add.writable, descriptors.add.configurable);
console.log(descriptors.add.value === originalAdd);
const result = Object.create(prototype, Object.getOwnPropertyDescriptors(value));
console.log(result.doubled, result.add(2), value.value);
result.doubled = 20;
console.log(JSON.stringify(result), result.value, result.add === value.add);
console.log(result instanceof Item, Object.create(null) instanceof Item);
return result;
}
copy(new Item());
function isItem(value) { return value instanceof Item; }
console.log(isItem(null), isItem(3), isItem({}));
class SpecialItem extends Item {
label() { return "special:" + this.value; }
}
function copySpecial(value) {
const prototype = Object.getPrototypeOf(value);
const result = Object.create(prototype, Object.getOwnPropertyDescriptors(value));
const parent = Object.getPrototypeOf(prototype);
console.log(Object.getOwnPropertyNames(parent).join(","));
console.log(result.label(), result.add(1), result instanceof SpecialItem, result instanceof Item);
}
copySpecial(new SpecialItem());
@@ -0,0 +1,19 @@
function make(value) {
return class Base {
static value = value;
value() { return value; }
};
}
function derive(value) {
return class Child extends make(value) {
static label = "child";
};
}
const Text = derive("text");
const Count = derive(42);
console.log(Text.label, Text.value, new Text().value());
console.log(Count.label, Count.value, new Count().value());
class Middle extends make("ancestor") {}
class Descendant extends Middle {}
console.log(new Descendant().value());
@@ -0,0 +1,31 @@
class Base {
constructor() { console.log("base"); }
}
class Forward extends Base {
constructor() {
super(...arguments);
console.log("forward", arguments.length);
}
}
new Forward(1, 2);
function values() {
console.log("values");
return [1, 2];
}
class Spread extends Base {
constructor() {
super(...values());
console.log("spread");
}
}
new Spread();
class Empty {}
class EmptyForward extends Empty {
constructor() { super(...arguments); }
}
console.log(new EmptyForward("value") instanceof Empty);
function invalid() { return 1; }
class Invalid extends Base {
constructor() { super(...invalid()); }
}
try { new Invalid(); } catch (error) { console.log(error.name); }
+12
View File
@@ -0,0 +1,12 @@
const key = Symbol.for("value");
class Box {
value = 1;
get [key]() { return this.value; }
set [key](value: number) { this.value = value; }
}
function check(box: any): void {
console.log(box[key], key in box, Object.hasOwn(box, key));
box[key] = 42;
console.log(box[key]);
}
check(new Box());
+31
View File
@@ -0,0 +1,31 @@
const hash = Symbol.for("hash");
function cache(self, value) {
Object.defineProperty(self, hash, { value() { return value; }, enumerable: false });
}
class Key {
calls = 0;
[hash]() {
this.calls++;
cache(this, 42);
return 42;
}
}
function read(value) { return value[hash](); }
function own(value) { return Object.hasOwn(value, hash); }
const key = new Key();
console.log(own(key), read(key), read(key), key.calls, own(key));
// A base constructor observes the instance before derived fields initialize.
// Copying properties must not expose an uninitialized native symbol slot.
const tag = Symbol.for("tag");
class AssignedError extends Error {
constructor(properties) {
super();
Object.assign(this, properties);
}
}
class TaggedError extends AssignedError {
[tag] = tag;
}
const failure = new TaggedError({ message: "copied" });
console.log(failure.message, failure[tag] === tag);
@@ -0,0 +1,19 @@
function select(reverse: boolean, first: string, second: string): string {
const [left, right] = reverse ? [second, first] : [first, second];
return left.toUpperCase() + ":" + right.toLowerCase();
}
console.log(select(false, "One", "Two"), select(true, "One", "Two"));
function choose(reverse: boolean) {
const a = { value: 1 }, b = { value: 2 };
const [left, right, amount] = reverse ? [b, a, 3] : [a, b, 4];
left.value += amount;
console.log(a.value, b.value, right.value);
}
choose(false);
choose(true);
const [only] = [...new Set(["one"])];
console.log(only);
const [first, ...rest] = [...new Set(["a", "b", "c"])];
console.log(first, rest.join(","));
+38
View File
@@ -0,0 +1,38 @@
class Explained extends Error {
detail = "computed";
get message() { return this.detail; }
toString() { return this.name + ": " + this.message; }
}
function read(error) { return error.message; }
const absent = new Explained();
const present = new Explained("own");
const empty = new Explained("");
console.log(absent.message, read(absent), absent.toString());
console.log(present.message, read(present), empty.message === "");
console.log(Object.hasOwn(absent, "message"), Object.hasOwn(present, "message"), Object.hasOwn(empty, "message"));
console.log(Object.getOwnPropertyDescriptor(absent, "message") === undefined);
console.log(Object.getOwnPropertyDescriptor(present, "message").enumerable);
function write(error, message) { error.message = message; }
try { write(absent, "no"); } catch (error) { console.log(error.name); }
write(present, "updated");
console.log(read(present));
const marker = Symbol("marker");
Object.assign(absent, { detail: "assigned" });
Object.defineProperty(absent, marker, { value: 42 });
console.log(read(absent), absent[marker]);
try { throw absent; } catch (error) {
console.log(error === absent, error.message, error[marker]);
}
class DefaultRendering extends Error {
get message() { return "from getter"; }
}
console.log(new DefaultRendering().toString());
class ThrowingMessage extends Error {
/** @returns {string} */
get message() { throw new Error("message getter"); }
}
try { new ThrowingMessage().toString(); } catch (error) { console.log(error.message); }
try { new ThrowingMessage().stack; } catch (error) { console.log(error.message); }
const plain = new Error();
write(plain, "added");
console.log(Object.hasOwn(plain, "message"), Object.getOwnPropertyDescriptor(plain, "message").enumerable);
+18
View File
@@ -0,0 +1,18 @@
class Failure extends Error {
get name() { return "Failure"; }
}
const error = new Failure("bad");
console.log(error.name, error.toString(), Object.hasOwn(error, "name"));
const seen = [];
class NameThrows extends Error {
/** @returns {string} */
get name() { seen.push("name"); throw new Error("name getter"); }
get message() { seen.push("message"); return "unused"; }
}
try { new NameThrows().toString(); } catch (error) { console.log(error.message); }
console.log(seen.join(","));
function readMessage(error) { return error.message; }
const renamed = new Error();
renamed.name = "Renamed";
console.log(renamed.name, readMessage(renamed) === "", Object.hasOwn(renamed, "message"));
+11
View File
@@ -0,0 +1,11 @@
function format(value) {
const error = new Error(value);
error.span = "context";
error.stack = "custom stack";
for (const key of ["span", "stack", "other"]) console.log(key, key in error);
console.log(error.span, error.stack);
const key = "extra";
error[key] = 42;
console.log(error[key]);
}
format("bad");
+1
View File
@@ -8,6 +8,7 @@ function make(label) {
const first = make("one");
const second = make("two");
function print(value) {
console.log(value instanceof Error, String(value));
console.log(value.toString());
const method = value.toString;
console.log(method.call(value));
+16
View File
@@ -0,0 +1,16 @@
const apply = Function.prototype.apply;
function method(a, b) { return this.prefix + a + b; }
function read(fn) { return fn.apply; }
console.log(read(method) === apply);
console.log(apply.call(method, { prefix: "sum:" }, [2, 3]));
console.log(apply.call(method, { prefix: "object:" }, { 0: 2, 1: 3, length: 2 }));
console.log(method.apply({ prefix: "direct:" }, { 0: 2, 1: 3, length: 2 }));
const key = Symbol("apply");
method[key] = apply;
console.log(method[key]({ prefix: "next:" }, [4, 5]));
function remove(fn, symbol) { delete fn[symbol]; }
remove(method, key);
const assign = Object.assign;
const target = { a: 1 };
console.log(assign(target, { b: 2 }, { a: 3 }) === target);
console.log(JSON.stringify(target));

Some files were not shown because too many files have changed in this diff Show More