mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
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:
@@ -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}`);
|
||||
|
||||
@@ -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}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
`}`,
|
||||
``,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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]!;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+535
-55
File diff suppressed because it is too large
Load Diff
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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("|"));
|
||||
@@ -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));
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]);
|
||||
@@ -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]);
|
||||
@@ -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 });
|
||||
@@ -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));
|
||||
@@ -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); }
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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); }
|
||||
@@ -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());
|
||||
@@ -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(","));
|
||||
@@ -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);
|
||||
@@ -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"));
|
||||
@@ -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");
|
||||
@@ -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));
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user