diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b95a4001..4beec267c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts index 7dc27a56b..799af9319 100644 --- a/packages/compiler/src/backend/cycle-analysis.ts +++ b/packages/compiler/src/backend/cycle-analysis.ts @@ -64,6 +64,7 @@ export function computeTraced(mod: IrModule): { shapes: Set; unions: Set case "dyn": case "classval": case "promise": + case "caught": return true; case "object": return tracedShapes.has(`object:${t.className}`); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 931f315bd..3bded59e6 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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}`, ``, ); } diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 6fc828aae..52d205c5d 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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(); private readonly dynFuncAdapters = new Map(); private readonly promiseDynAdapters = new Map(); + private readonly mapDynOps = new Map(); private dynToStrFn: string | null = null; private caughtToDynFn: string | null = null; private readonly helperMemo = new Map(); @@ -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(), `}`, ``, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 292effba5..cc8a83b3f 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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()`); diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index 6bb659d34..bb2a3e581 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -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; diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 0967350e6..27bfd29a1 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 1a5d541a8..eb4e7a8e7 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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)`); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 57b3712c2..9a512db81 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -537,6 +537,7 @@ export const LIB_FN_SYMS: Record = { "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 = { "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 = { "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 = { "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 = { "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", diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 4a5c2bd1d..7cddfd26d 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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"; diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts new file mode 100644 index 000000000..e09cdfd22 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-construction.ts @@ -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(); + private readonly completed = new Map>(); + private prototypeHelper: IrFunction | null = null; + private readonly prototypes = new Set(); + + 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(); + 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; + } +} diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index 1b173cb36..54dca3f47 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index fe5af7c69..0dfac99b2 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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; interface Dispatch { @@ -34,14 +35,31 @@ interface PropertyDispatch { classes: Set; } -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(); private readonly dispatches = new Map(); private readonly properties = new Map(); @@ -79,10 +98,17 @@ export class ClassDynamicDispatch { private readonly constructors = new Map(); private readonly constructed = new Set(); private readonly instanceTests = new Map>(); + private readonly jsonPrototypes = new Set(); + private descriptorHelper: IrFunction | null = null; + private prototypeHelper: IrFunction | null = null; + private readonly prototypeClasses = new Set(); + private readonly instancePrototypes = new Map }>(); 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(); const rewrite = new Set(); 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 = { @@ -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, 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); diff --git a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts index 84ea137eb..664d298ed 100644 --- a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts +++ b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index b94e95970..29763895f 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -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])); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 5cc1b610f..f34c274f2 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -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) { diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts new file mode 100644 index 000000000..a0927df9b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -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 }; +} diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts index 08c885750..b59b780b9 100644 --- a/packages/compiler/src/frontend/lowering/class-static-data.ts +++ b/packages/compiler/src/frontend/lowering/class-static-data.ts @@ -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`; diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 47745e0bc..8c55542b7 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -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) => diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index 1d075b2b1..908740fdf 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 47c4f576f..13b3649fb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index ad3e317ea..e39c381bc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts index 6c3354ba8..f9e751930 100644 --- a/packages/compiler/src/frontend/lowering/function-context.ts +++ b/packages/compiler/src/frontend/lowering/function-context.ts @@ -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; +} diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 6985515ea..f611c384b 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -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(); private readonly dispatches = new Map }>(); private readonly generated = new Set(); + private readonly properties = new Map }>(); process(lowerer: Lowerer, functions: readonly IrFunction[], classHelpers: ReadonlySet): boolean { const seen = new Set(); @@ -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); diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index 0ed68bd22..887e4df95 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -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 = { + 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()): 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); diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index 2e7f4754a..9d2ba13fa 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -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/Promise) 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) }; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 390841b80..4540f8828 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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, infer: NonNullable, 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); } diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 2a4004317..b08292901 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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; + prototypeReflectionReady?: true; + prototypeReflectionRefusal?: string; localClass?: { context: FnCtx | null; bodies: Map; @@ -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 = 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"); } diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index 3b66f87bf..51d217c91 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -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 (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 { 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", diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index bf9422027..b2bfba7eb 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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 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 reading (promise — 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 diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index f70584838..3a7ec93cf 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/lower-island.ts b/packages/compiler/src/frontend/lowering/lower-island.ts index be2a658c2..0ec5378d5 100644 --- a/packages/compiler/src/frontend/lowering/lower-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-island.ts @@ -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({ diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index e7adde429..6280c77ab 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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(); + 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 diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 2e4ccb974..73e6b42f3 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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 && diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index e4c390161..1b0db95e7 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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(); + readonly preparingClassFactories = new Set(); /** 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(); + 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(); + readonly computedCallableBases = new Map(); /** 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; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 5466827c0..5457920c1 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -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 = { + 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 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 diff --git a/packages/compiler/src/frontend/lowering/unregistered-class-types.test.ts b/packages/compiler/src/frontend/lowering/unregistered-class-types.test.ts index fb8d640f8..7b8cfebdb 100644 --- a/packages/compiler/src/frontend/lowering/unregistered-class-types.test.ts +++ b/packages/compiler/src/frontend/lowering/unregistered-class-types.test.ts @@ -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(); - 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); diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 31b9647c5..9e57248bb 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -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(); 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 | 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(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 = 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 = 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 => diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 803c83f05..977496f5c 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -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` is - * Generator), 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, AsyncGenerator (and // the lib's IterableIterator, 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` 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 diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 74f71d477..14a9c242e 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -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; diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 7164bc0d6..762a6792e 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -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(); + const unions = new Map(); + 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([ + ["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(); + 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 }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 50aa22c7a..34c4a53a8 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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` stays fenced). + * return); checking OUT preserves the boxed Promise 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 = 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 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 = 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, ): 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 = 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): 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, ): 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 = 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 = 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 = 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 = 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 = new Set([ "dyn.getPrototype", "dyn.setPrototype", "dyn.getOwnPropertyNames", + "dyn.ownKeys", "dyn.getOwnPropertySymbols", "dyn.getOwnPropertyDescriptors", "dyn.preventExtensions", diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts index 8567826f8..da9e59d78 100644 --- a/packages/compiler/src/ir/validate.test.ts +++ b/packages/compiler/src/ir/validate.test.ts @@ -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]!; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 420a9264d..af156519c 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -126,6 +126,7 @@ export const LIB_FN_SIGS: Record 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(); 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([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"), ) ) { diff --git a/packages/compiler/test/json-callbacks.test.ts b/packages/compiler/test/json-callbacks.test.ts index 3d5ec2a55..fdaf10ab8 100644 --- a/packages/compiler/test/json-callbacks.test.ts +++ b/packages/compiler/test/json-callbacks.test.ts @@ -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 }); diff --git a/packages/compiler/test/native-class-descriptors.test.ts b/packages/compiler/test/native-class-descriptors.test.ts index 4e6a33061..09172c8bb 100644 --- a/packages/compiler/test/native-class-descriptors.test.ts +++ b/packages/compiler/test/native-class-descriptors.test.ts @@ -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()); diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index 52c2ef849..09544889f 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -8662,6 +8662,12 @@ ], "diags": [] }, + "/tests/corpus/arguments-arrow-capture.mjs": { + "order": [ + "/tests/corpus/arguments-arrow-capture.mjs" + ], + "diags": [] + }, "/tests/corpus/arguments-reexport/main.ts": { "order": [ "/tests/corpus/arguments-reexport/core.js", @@ -8682,6 +8688,12 @@ ], "diags": [] }, + "/tests/corpus/array-checked-callback-results.mjs": { + "order": [ + "/tests/corpus/array-checked-callback-results.mjs" + ], + "diags": [] + }, "/tests/corpus/array-checked-constructor.js": { "order": [ "/tests/corpus/array-checked-constructor.js" @@ -8700,6 +8712,12 @@ ], "diags": [] }, + "/tests/corpus/array-from-value.mjs": { + "order": [ + "/tests/corpus/array-from-value.mjs" + ], + "diags": [] + }, "/tests/corpus/array-isarray-value/main.js": { "order": [ "/tests/corpus/array-isarray-value/main.js" @@ -8772,6 +8790,12 @@ ], "diags": [] }, + "/tests/corpus/builtin-prototype-method-values.mjs": { + "order": [ + "/tests/corpus/builtin-prototype-method-values.mjs" + ], + "diags": [] + }, "/tests/corpus/callable-module-members.mjs": { "order": [ "/tests/corpus/callable-module-members.mjs" @@ -8798,6 +8822,12 @@ ], "diags": [] }, + "/tests/corpus/catch-capture.ts": { + "order": [ + "/tests/corpus/catch-capture.ts" + ], + "diags": [] + }, "/tests/corpus/checked-addition.js": { "order": [ "/tests/corpus/checked-addition.js" @@ -8810,6 +8840,19 @@ ], "diags": [] }, + "/tests/corpus/checked-array-assignment.ts": { + "order": [ + "/tests/corpus/checked-array-assignment.ts" + ], + "diags": [] + }, + "/tests/corpus/checked-array-flat/main.ts": { + "order": [ + "/tests/corpus/checked-array-flat/operations.mjs", + "/tests/corpus/checked-array-flat/main.ts" + ], + "diags": [] + }, "/tests/corpus/checked-bitwise/main.js": { "order": [ "/tests/corpus/checked-bitwise/main.js" @@ -8829,12 +8872,48 @@ ], "diags": [] }, + "/tests/corpus/checked-callback-options.ts": { + "order": [ + "/tests/corpus/checked-callback-options.ts" + ], + "diags": [] + }, + "/tests/corpus/checked-class-descriptors.mjs": { + "order": [ + "/tests/corpus/checked-class-descriptors.mjs" + ], + "diags": [] + }, + "/tests/corpus/checked-class-factory.mjs": { + "order": [ + "/tests/corpus/checked-class-factory.mjs" + ], + "diags": [] + }, + "/tests/corpus/checked-collection-iterators.ts": { + "order": [ + "/tests/corpus/checked-collection-iterators.ts" + ], + "diags": [] + }, + "/tests/corpus/checked-collection-symbol-methods.ts": { + "order": [ + "/tests/corpus/checked-collection-symbol-methods.ts" + ], + "diags": [] + }, "/tests/corpus/checked-increment/main.js": { "order": [ "/tests/corpus/checked-increment/main.js" ], "diags": [] }, + "/tests/corpus/checked-inherited-fields.ts": { + "order": [ + "/tests/corpus/checked-inherited-fields.ts" + ], + "diags": [] + }, "/tests/corpus/checked-numeric-arithmetic.js": { "order": [ "/tests/corpus/checked-numeric-arithmetic.js" @@ -8847,6 +8926,31 @@ ], "diags": [] }, + "/tests/corpus/checked-optional-calls.mjs": { + "order": [ + "/tests/corpus/checked-optional-calls.mjs" + ], + "diags": [] + }, + "/tests/corpus/checked-promise-recovery.ts": { + "order": [ + "/tests/corpus/checked-promise-recovery.ts" + ], + "diags": [] + }, + "/tests/corpus/checked-proxy-callback/main.ts": { + "order": [ + "/tests/corpus/checked-proxy-callback/operations.mjs", + "/tests/corpus/checked-proxy-callback/main.ts" + ], + "diags": [] + }, + "/tests/corpus/checked-record-constructor.ts": { + "order": [ + "/tests/corpus/checked-record-constructor.ts" + ], + "diags": [] + }, "/tests/corpus/checked-renderer-defaults.js": { "order": [ "/tests/corpus/checked-renderer-defaults.js" @@ -8859,6 +8963,12 @@ ], "diags": [] }, + "/tests/corpus/checked-symbol-array.ts": { + "order": [ + "/tests/corpus/checked-symbol-array.ts" + ], + "diags": [] + }, "/tests/corpus/class-accessor-returned-function.js": { "order": [ "/tests/corpus/class-accessor-returned-function.js" @@ -8913,12 +9023,48 @@ ], "diags": [] }, + "/tests/corpus/class-curried-factory.mjs": { + "order": [ + "/tests/corpus/class-curried-factory.mjs" + ], + "diags": [] + }, "/tests/corpus/class-data-descriptors.js": { "order": [ "/tests/corpus/class-data-descriptors.js" ], "diags": [] }, + "/tests/corpus/class-factory-inherited-statics.mjs": { + "order": [ + "/tests/corpus/class-factory-inherited-statics.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-factory-parameter-base.mjs": { + "order": [ + "/tests/corpus/class-factory-parameter-base.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-factory-record-base.mjs": { + "order": [ + "/tests/corpus/class-factory-record-base.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-factory-recursive-statics.mjs": { + "order": [ + "/tests/corpus/class-factory-recursive-statics.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-factory-static-members.mjs": { + "order": [ + "/tests/corpus/class-factory-static-members.mjs" + ], + "diags": [] + }, "/tests/corpus/class-function-base/main.js": { "order": [ "/tests/corpus/class-function-base/base.js", @@ -8938,6 +9084,18 @@ ], "diags": [] }, + "/tests/corpus/class-json-checked.ts": { + "order": [ + "/tests/corpus/class-json-checked.ts" + ], + "diags": [] + }, + "/tests/corpus/class-local-function-base.mjs": { + "order": [ + "/tests/corpus/class-local-function-base.mjs" + ], + "diags": [] + }, "/tests/corpus/class-property-call-values.mjs": { "order": [ "/tests/corpus/class-property-call-values.mjs" @@ -8950,12 +9108,42 @@ ], "diags": [] }, + "/tests/corpus/class-prototype-descriptor-copy.mjs": { + "order": [ + "/tests/corpus/class-prototype-descriptor-copy.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-specialized-computed-base.mjs": { + "order": [ + "/tests/corpus/class-specialized-computed-base.mjs" + ], + "diags": [] + }, "/tests/corpus/class-static-data.js": { "order": [ "/tests/corpus/class-static-data.js" ], "diags": [] }, + "/tests/corpus/class-super-surplus-spread.mjs": { + "order": [ + "/tests/corpus/class-super-surplus-spread.mjs" + ], + "diags": [] + }, + "/tests/corpus/class-symbol-accessors.ts": { + "order": [ + "/tests/corpus/class-symbol-accessors.ts" + ], + "diags": [] + }, + "/tests/corpus/class-symbol-cache.mjs": { + "order": [ + "/tests/corpus/class-symbol-cache.mjs" + ], + "diags": [] + }, "/tests/corpus/closure-nullable-union-return.ts": { "order": [ "/tests/corpus/closure-nullable-union-return.ts" @@ -9004,6 +9192,12 @@ ], "diags": [] }, + "/tests/corpus/conditional-tuple-destructuring.ts": { + "order": [ + "/tests/corpus/conditional-tuple-destructuring.ts" + ], + "diags": [] + }, "/tests/corpus/console-imports.cjs": { "order": [ "/tests/corpus/console-imports.cjs" @@ -9082,6 +9276,24 @@ ], "diags": [] }, + "/tests/corpus/error-inherited-message.mjs": { + "order": [ + "/tests/corpus/error-inherited-message.mjs" + ], + "diags": [] + }, + "/tests/corpus/error-inherited-name.mjs": { + "order": [ + "/tests/corpus/error-inherited-name.mjs" + ], + "diags": [] + }, + "/tests/corpus/error-property-writes.mjs": { + "order": [ + "/tests/corpus/error-property-writes.mjs" + ], + "diags": [] + }, "/tests/corpus/error-prototype-tostring.js": { "order": [ "/tests/corpus/error-prototype-tostring.js" @@ -9178,6 +9390,18 @@ ], "diags": [] }, + "/tests/corpus/function-apply-value.mjs": { + "order": [ + "/tests/corpus/function-apply-value.mjs" + ], + "diags": [] + }, + "/tests/corpus/function-constructor-global.mjs": { + "order": [ + "/tests/corpus/function-constructor-global.mjs" + ], + "diags": [] + }, "/tests/corpus/function-constructor-kinds/main.js": { "order": [ "/tests/corpus/function-constructor-kinds/consumer.js", @@ -9191,6 +9415,12 @@ ], "diags": [] }, + "/tests/corpus/function-custom-to-string.mjs": { + "order": [ + "/tests/corpus/function-custom-to-string.mjs" + ], + "diags": [] + }, "/tests/corpus/function-object-assign.js": { "order": [ "/tests/corpus/function-object-assign.js" @@ -9204,6 +9434,18 @@ ], "diags": [] }, + "/tests/corpus/generator-checked-protocol.ts": { + "order": [ + "/tests/corpus/generator-checked-protocol.ts" + ], + "diags": [] + }, + "/tests/corpus/generator-checked-return.js": { + "order": [ + "/tests/corpus/generator-checked-return.js" + ], + "diags": [] + }, "/tests/corpus/generator-custom-delegation/main.ts": { "order": [ "/tests/corpus/generator-custom-delegation/iterators.js", @@ -9211,6 +9453,12 @@ ], "diags": [] }, + "/tests/corpus/generator-default-return-pattern.ts": { + "order": [ + "/tests/corpus/generator-default-return-pattern.ts" + ], + "diags": [] + }, "/tests/corpus/generator-delegation-values.ts": { "order": [ "/tests/corpus/generator-delegation-values.ts" @@ -9317,6 +9565,78 @@ ], "diags": [] }, + "/tests/corpus/inferred-array-assignment-values.mjs": { + "order": [ + "/tests/corpus/inferred-array-assignment-values.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-class-default.mjs": { + "order": [ + "/tests/corpus/inferred-class-default.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-compound-values.mjs": { + "order": [ + "/tests/corpus/inferred-compound-values.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-conditional-class.mjs": { + "order": [ + "/tests/corpus/inferred-conditional-class.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-error-fields.js": { + "order": [ + "/tests/corpus/inferred-error-fields.js" + ], + "diags": [] + }, + "/tests/corpus/inferred-map-patterns.mjs": { + "order": [ + "/tests/corpus/inferred-map-patterns.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-method-override.js": { + "order": [ + "/tests/corpus/inferred-method-override.js" + ], + "diags": [] + }, + "/tests/corpus/inferred-partial-destructuring.mjs": { + "order": [ + "/tests/corpus/inferred-partial-destructuring.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-primitive-properties.mjs": { + "order": [ + "/tests/corpus/inferred-primitive-properties.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-record-return.mjs": { + "order": [ + "/tests/corpus/inferred-record-return.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-recursive-record.mjs": { + "order": [ + "/tests/corpus/inferred-recursive-record.mjs" + ], + "diags": [] + }, + "/tests/corpus/inferred-undefined-field.mjs": { + "order": [ + "/tests/corpus/inferred-undefined-field.mjs" + ], + "diags": [] + }, "/tests/corpus/instanceof-class-record-union.ts": { "order": [ "/tests/corpus/instanceof-class-record-union.ts" @@ -9524,6 +9844,12 @@ ], "diags": [] }, + "/tests/corpus/json-runtime-options.mjs": { + "order": [ + "/tests/corpus/json-runtime-options.mjs" + ], + "diags": [] + }, "/tests/corpus/json-stringify-value.js": { "order": [ "/tests/corpus/json-stringify-value.js" @@ -9851,6 +10177,12 @@ ], "diags": [] }, + "/tests/corpus/nullable-object-extension.mjs": { + "order": [ + "/tests/corpus/nullable-object-extension.mjs" + ], + "diags": [] + }, "/tests/corpus/nullish-assignment-values.ts": { "order": [ "/tests/corpus/nullish-assignment-values.ts" @@ -9881,6 +10213,12 @@ ], "diags": [] }, + "/tests/corpus/object-prototype-identity.mjs": { + "order": [ + "/tests/corpus/object-prototype-identity.mjs" + ], + "diags": [] + }, "/tests/corpus/object-reexport/main.js": { "order": [ "/tests/corpus/object-reexport/core.js", @@ -9902,6 +10240,12 @@ ], "diags": [] }, + "/tests/corpus/optional-callback-arity.ts": { + "order": [ + "/tests/corpus/optional-callback-arity.ts" + ], + "diags": [] + }, "/tests/corpus/optional-native-callables.js": { "order": [ "/tests/corpus/optional-native-callables.js" @@ -10004,6 +10348,18 @@ ], "diags": [] }, + "/tests/corpus/promise-checked-rejections.ts": { + "order": [ + "/tests/corpus/promise-checked-rejections.ts" + ], + "diags": [] + }, + "/tests/corpus/promise-resolve-void.ts": { + "order": [ + "/tests/corpus/promise-resolve-void.ts" + ], + "diags": [] + }, "/tests/corpus/prototype-method-overrides.mjs": { "order": [ "/tests/corpus/prototype-method-overrides.mjs" @@ -10028,6 +10384,24 @@ ], "diags": [] }, + "/tests/corpus/reflect-own-keys.mjs": { + "order": [ + "/tests/corpus/reflect-own-keys.mjs" + ], + "diags": [] + }, + "/tests/corpus/reflect-record-union-keys.mjs": { + "order": [ + "/tests/corpus/reflect-record-union-keys.mjs" + ], + "diags": [] + }, + "/tests/corpus/reflect-typed-record-keys.ts": { + "order": [ + "/tests/corpus/reflect-typed-record-keys.ts" + ], + "diags": [] + }, "/tests/corpus/regex-checked-casts.ts": { "order": [ "/tests/corpus/regex-checked-casts.ts" @@ -10173,6 +10547,12 @@ ], "diags": [] }, + "/tests/corpus/string-split-checked-separator.mjs": { + "order": [ + "/tests/corpus/string-split-checked-separator.mjs" + ], + "diags": [] + }, "/tests/corpus/structural-class-properties.ts": { "order": [ "/tests/corpus/structural-class-properties.ts" @@ -10310,6 +10690,13 @@ ], "diags": [] }, + "/tests/corpus/typed-collection-views/main.ts": { + "order": [ + "/tests/corpus/typed-collection-views/use.mjs", + "/tests/corpus/typed-collection-views/main.ts" + ], + "diags": [] + }, "/tests/corpus/typed-prototype-methods.ts": { "order": [ "/tests/corpus/typed-prototype-methods.ts" @@ -10436,6 +10823,12 @@ ], "diags": [] }, + "/tests/corpus/weak-native-class-identity.ts": { + "order": [ + "/tests/corpus/weak-native-class-identity.ts" + ], + "diags": [] + }, "/tests/corpus/weak-set-native.js": { "order": [ "/tests/corpus/weak-set-native.js" diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 9c51353a3..5c23efbdb 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -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()); } diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 334862c26..f07340e6e 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -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) { diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 8969614f7..b35e678f7 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -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 #"; + 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); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 741b823da..3ba2ce7b9 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -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(); diff --git a/packages/runtime/src/scr_error.c b/packages/runtime/src/scr_error.c index 99a3be040..09b02fe9a 100644 --- a/packages/runtime/src/scr_error.c +++ b/packages/runtime/src/scr_error.c @@ -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); } diff --git a/packages/runtime/src/scr_exception.c b/packages/runtime/src/scr_exception.c index 7425913ec..e98c8fcab 100644 --- a/packages/runtime/src/scr_exception.c +++ b/packages/runtime/src/scr_exception.c @@ -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); } } diff --git a/packages/runtime/src/scr_island.c b/packages/runtime/src/scr_island.c index d325c3754..826efd019 100644 --- a/packages/runtime/src/scr_island.c +++ b/packages/runtime/src/scr_island.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) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index e311348d6..8182031ec 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -23,10 +23,17 @@ */ #include "scr_runtime.h" +static SCR_TL ScrDyn *scr_builtin_object_prototype; + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { - if (!value || value->kind != SCR_DYN_FUNC || !value->v.fn.class_obj) return false; + if (value && value == scr_builtin_object_prototype) { + static const char message[] = "scriptc: Object.prototype reflection and mutation are not supported yet"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return true; + } + if (!value || value->kind != SCR_DYN_FUNC || !value->v.fn.class_obj || value->v.fn.class_obj->static_data) return false; static const char message[] = "scriptc: class reflection through unknown is not supported"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return true; @@ -49,11 +56,21 @@ static void scr_json_oom(void) { } static SCR_TL ScrDyn *scr_iterator_symbol; +static SCR_TL ScrDyn *scr_async_iterator_symbol; +static bool scr_dyn_generator(const ScrDyn *value) { + return value && value->kind == SCR_DYN_TYPED_REF && + (!strncmp(value->v.typed_ref.type_key, "generator<", 10) || !strncmp(value->v.typed_ref.type_key, "async-generator<", 16)); +} +static bool scr_dyn_generator_symbol(const ScrDyn *value, const ScrDyn *key) { + const ScrDyn *symbol = value->v.typed_ref.type_key[0] == 'a' ? scr_async_iterator_symbol : scr_iterator_symbol; + return symbol && key->v.symbol.value == symbol->v.symbol.value; +} static ScrDyn *scr_builtin_iterator_method(const ScrDyn *value); static bool scr_builtin_iterable(const ScrDyn *value) { return value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_STR || value->kind == SCR_DYN_BYTES || + (scr_dyn_generator(value) && value->v.typed_ref.type_key[0] != 'a') || (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value)) || - (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || + (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_ITERATOR || value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(value)->iter_pack)); } static bool scr_iterator_object(const ScrDyn *value) { @@ -82,6 +99,17 @@ static void scr_iterator_symbol_cleanup(void) { scr_iterator_symbol = NULL; } +static void scr_async_iterator_symbol_cleanup(void) { + scr_dyn_release(scr_async_iterator_symbol); + scr_async_iterator_symbol = NULL; +} + +void scr_dyn_install_async_iterator_symbol(ScrDyn *key) { + if (scr_async_iterator_symbol) return; + scr_async_iterator_symbol = scr_dyn_retain(key); + scr_atexit(scr_async_iterator_symbol_cleanup); +} + void scr_dyn_install_iterator_symbol(ScrDyn *key) { if (scr_iterator_symbol) return; scr_iterator_symbol = scr_dyn_retain(key); @@ -793,6 +821,10 @@ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) { return scr_dyn_not_iterable(src, msg); } +ScrDyn *scr_dyn_jsval_iter_n(const ScrDyn *src, double count) { + return scr_dyn_jsval_ops()->iter_n(src->v.jsval.cell, count); +} + /* The for-of-over-dyn pack accessors: the emitted index loop drives them * over a scr_dyn_iter_pack result (ARR by construction — the defensive * arms cover nothing reachable from that lowering). Never throw. */ @@ -966,6 +998,21 @@ static void scr_builtin_array_prototype_cleanup(void) { scr_dyn_release(prototype); } +static void scr_builtin_object_prototype_cleanup(void) { + ScrDyn *prototype = scr_builtin_object_prototype; + scr_builtin_object_prototype = NULL; + scr_dyn_release(prototype); +} + +ScrDyn *scr_dyn_object_prototype(void) { + if (!scr_builtin_object_prototype) { + scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ); + scr_builtin_object_prototype->null_proto = true; + scr_atexit(scr_builtin_object_prototype_cleanup); + } + return scr_dyn_retain(scr_builtin_object_prototype); +} + ScrDyn *scr_dyn_array_prototype_base(void) { if (!scr_builtin_array_prototype) { scr_builtin_array_prototype = scr_dyn_alloc(SCR_DYN_ARR); @@ -1005,7 +1052,7 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) { } ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) { - if (scr_dyn_class_reflection_fence(prototype)) return NULL; + if (prototype != scr_builtin_object_prototype && scr_dyn_class_reflection_fence(prototype)) return NULL; if (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_ARR && prototype->kind != SCR_DYN_NULL) { static const char message[] = "Object prototype may only be an Object or null"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -1017,7 +1064,15 @@ ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) { } ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { + if (object == scr_builtin_object_prototype) return scr_dyn_new_null(); if (scr_dyn_class_reflection_fence(object)) return NULL; + if (object->kind == SCR_DYN_TYPED_REF && strncmp(object->v.typed_ref.type_key, "object:", 7)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + if (!view) return NULL; + ScrDyn *result = scr_dyn_get_prototype(view); + scr_dyn_release(view); + return result; + } if (object->kind == SCR_DYN_FUNC && object->v.fn.clo->props) { ScrDyn *table = scr_dyn_fn_properties(object); ScrDyn *result = table->prototype ? scr_dyn_retain(table->prototype) @@ -1032,7 +1087,9 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { if (object->kind == SCR_DYN_OBJ) { if (object->prototype) return scr_dyn_retain(object->prototype); if (object->null_proto) return scr_dyn_new_null(); + return scr_dyn_object_prototype(); } + if (object == scr_builtin_array_prototype) return scr_dyn_object_prototype(); if (object->kind == SCR_DYN_NULL || object->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); @@ -1059,17 +1116,23 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { return scr_dyn_retain(object); } if (object->kind != SCR_DYN_OBJ || - (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_NULL)) { + (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_FUNC && prototype->kind != SCR_DYN_NULL)) { static const char message[] = "Object prototype may only be an Object or null"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - for (ScrDyn *current = prototype; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { + for (ScrDyn *current = prototype; current;) { + if (current->kind == SCR_DYN_FUNC) { + ScrDyn *table = scr_dyn_fn_properties(current); + current = table; + scr_dyn_release(table); /* the function owns the table */ + } if (current == object) { static const char message[] = "Cyclic __proto__ value"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } + current = current->prototype; } if (object->non_extensible && (prototype->kind == SCR_DYN_NULL ? object->prototype != NULL || !object->null_proto : object->prototype != prototype)) { static const char message[] = "Cannot set prototype of a non-extensible object"; @@ -1131,6 +1194,12 @@ bool scr_dyn_is_sealed(const ScrDyn *object) { static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *key, size_t length) { for (const ScrDyn *current = object; current; current = current->prototype) { + if (current->kind == SCR_DYN_FUNC) { + ScrDyn *table = scr_dyn_fn_properties(current); + const ScrDyn *owner = scr_dyn_property_owner(table, key, length); + scr_dyn_release(table); + return owner; + } if (current->kind != SCR_DYN_OBJ) continue; for (size_t i = 0; i < current->v.obj.len; i++) { const ScrDynEntry *entry = ¤t->v.obj.entries[i]; @@ -1143,7 +1212,7 @@ static const ScrDyn *scr_dyn_property_owner(const ScrDyn *object, const char *ke static bool scr_dyn_has_object_prototype(const ScrDyn *object) { const ScrDyn *current = object; while (current->prototype) current = current->prototype; - return !current->null_proto; + return current == scr_builtin_object_prototype || !current->null_proto; } void scr_dyn_proxy_unsupported(const char *operation) { @@ -1808,6 +1877,7 @@ static ScrDyn *scr_dyn_class_call(ScrClosure *closure, ScrDyn *const *args, size ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key) { ScrClosure *closure = scr_closure_new(NULL, 1); + if (cls->static_data) closure->props = scr_box_retain(cls->static_data); closure->caps[0] = scr_box_new_obj(scr_classobj_retain_v, scr_classobj_release_v, scr_classobj_trace_v); scr_box_set_ref(closure->caps[0], scr_classobj_retain(cls)); ScrDyn *value = scr_dyn_new_func(closure, scr_dyn_class_call, (uint32_t)cls->length, type_key, cls->name->data); @@ -1820,6 +1890,10 @@ bool scr_dyn_class_is(const ScrDyn *value, const char *type_key) { strcmp(value->v.fn.sig, type_key) == 0; } +bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key) { + return scr_dyn_class_is(value, type_key->data); +} + ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, const ScrDynPath *path) { if (!scr_dyn_class_is(value, type_key)) { scr_dyn_check_fail(path, "class constructor", value); @@ -2213,10 +2287,12 @@ bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name) { } bool scr_dyn_isl_is_array(const ScrDyn *d) { + if (scr_dyn_generator(d)) return false; if (d->kind == SCR_DYN_TYPED_REF) { if (d->v.typed_ref.type_key_len >= 7 && memcmp(d->v.typed_ref.type_key, "object:", 7) == 0) return false; ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); + if (!materialized) return false; bool out = materialized->kind == SCR_DYN_ARR || scr_dyn_isl_is_array(materialized); scr_dyn_release(materialized); @@ -2226,6 +2302,13 @@ bool scr_dyn_isl_is_array(const ScrDyn *d) { } bool scr_dyn_isl_is_error(const ScrDyn *d) { + /* Registered native Error capsules retain their brand even when their + * public property view omits the internal error marker. */ + ScrError *error = scr_errdyn_err_of(d); + if (error) { + scr_error_release(error); + return true; + } if (d->kind == SCR_DYN_TYPED_REF) { ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); bool out = @@ -2403,6 +2486,10 @@ static bool scr_dyn_property_same_value(const ScrDyn *a, const ScrDyn *b) { return scr_dyn_strict_eq(a, b); } +bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right) { + return scr_dyn_property_same_value(left, right); +} + static void scr_dyn_descriptor_fields_drop(ScrDyn **fields) { for (size_t i = 0; i < 6; i++) scr_dyn_release(fields[i]); } @@ -2413,6 +2500,8 @@ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key); * the configurable, non-writable builtin members once so redefinitions and * deletions retain the same behavior through every alias. Returns +1. */ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { + if (!function->v.fn.clo->props && function->v.fn.class_obj && function->v.fn.class_obj->static_data) + function->v.fn.clo->props = scr_box_retain(function->v.fn.class_obj->static_data); if (!function->v.fn.clo->props) { ScrDyn *table = scr_dyn_new_obj(); const char *name = function->v.fn.name ? function->v.fn.name : ""; @@ -2427,6 +2516,7 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, &scr_dyn_trace_v); scr_box_set_ref(box, table); function->v.fn.clo->props = box; + if (function->v.fn.class_obj) function->v.fn.class_obj->static_data = scr_box_retain(box); if (function->v.fn.clo->function_kind & 4) { ScrDyn *prototype = scr_dyn_new_obj(); scr_dyn_obj_set(prototype, "constructor", 11, scr_dyn_retain((ScrDyn *)function)); @@ -2440,6 +2530,22 @@ static ScrDyn *scr_dyn_fn_properties(const ScrDyn *function) { return (ScrDyn *)scr_box_get_ref(function->v.fn.clo->props); } +ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) { + if (constructor->kind != SCR_DYN_FUNC || !constructor->v.fn.class_obj || (base->kind != SCR_DYN_FUNC && base->kind != SCR_DYN_UNDEF)) { + static const char message[] = "Class inheritance requires native constructors"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrDyn *table = scr_dyn_fn_properties(constructor); + if (base->kind == SCR_DYN_FUNC) scr_dyn_release(scr_dyn_fn_properties(base)); + ScrClassObj *cls = constructor->v.fn.class_obj; + if (!cls->static_data) cls->static_data = scr_box_retain(constructor->v.fn.clo->props); + ScrDyn *result = base->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_set_prototype(table, base); + scr_dyn_release(table); + scr_dyn_release(result); + return scr_exc_pending() ? NULL : scr_dyn_retain(constructor); +} + ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) { if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || constructor->v.fn.clo->function_kind != 4) { @@ -2494,6 +2600,12 @@ static ScrDynEntry *scr_dyn_symbol_entry(const ScrDyn *receiver, const ScrDyn *k ScrDynEntry *entry = NULL; for (const ScrDyn *current = receiver; current; current = own ? NULL : current->prototype ? current->prototype : current->kind == SCR_DYN_ARR && current != scr_builtin_array_prototype && !current->null_proto ? scr_builtin_array_prototype : NULL) { + if (current->kind == SCR_DYN_FUNC) { + ScrDyn *table = scr_dyn_fn_properties(current); + entry = scr_dyn_symbol_entry(table, key, own); + scr_dyn_release(table); + break; + } if (current->symbol_properties) entry = scr_dyn_entry(current->symbol_properties, name); if (entry) break; } @@ -2511,14 +2623,19 @@ typedef struct { size_t rc; ScrDyn *source; double index; - bool string_iterator; + unsigned kind; /* 0: array, 1: string, 2: Map, 3: Set */ + unsigned selection; /* 0: values, 1: keys, 2: entries */ + bool array_like; + double limit; } ScrNativeIterator; -static SCR_TL ScrDyn *scr_iterator_functions[7]; +static ScrDyn *scr_iterator_read(const ScrDyn *value, const char *key, size_t len); + +static SCR_TL ScrDyn *scr_iterator_functions[10]; static SCR_TL bool scr_iterator_functions_registered; static void scr_iterator_functions_cleanup(void) { - for (size_t i = 0; i < 7; i++) { + for (size_t i = 0; i < 10; i++) { ScrDyn *function = scr_iterator_functions[i]; scr_iterator_functions[i] = NULL; scr_dyn_release(function); @@ -2544,6 +2661,8 @@ static void *scr_native_iterator_retain(void *ptr) { static void scr_native_iterator_release(void *ptr) { ScrNativeIterator *iterator = ptr; if (--iterator->rc) return; + scr_weak_dispose(iterator); + if (iterator->source && iterator->kind >= 2) scr_map_iter_exit(iterator->source->v.handle.ptr); scr_dyn_release(iterator->source); free(iterator); } @@ -2555,14 +2674,29 @@ static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) { ScrDyn *view = source && source->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(source) : NULL; if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } ScrDyn *items = view ? view : source; - double length = !items ? 0 : items->kind == SCR_DYN_ARR ? (double)items->v.arr.len + ScrMap *map = source && iterator->kind >= 2 ? source->v.handle.ptr : NULL; + if (map) while (iterator->index < scr_map_iter_count(map) && !scr_map_iter_live(map, iterator->index)) iterator->index++; + double length = !items ? 0 : iterator->array_like ? iterator->limit : map ? scr_map_iter_count(map) : items->kind == SCR_DYN_ARR ? (double)items->v.arr.len : items->kind == SCR_DYN_BYTES ? (double)items->v.bytes->len : scr_str_utf16_len(items->v.str); bool done = !source || iterator->index >= length; ScrDyn *value; if (done) { value = scr_dyn_retain(scr_dyn_undefined()); iterator->source = NULL; + if (map) scr_map_iter_exit(map); scr_dyn_release(source); + } else if (iterator->array_like) { + char key[32]; + int size = snprintf(key, sizeof key, "%.0f", iterator->index++); + value = scr_iterator_read(items, key, (size_t)size); + } else if (map) { + double index = iterator->index++; + if (iterator->selection == 2) { + value = scr_dyn_new_arr(); + scr_dyn_arr_push(value, scr_map_dyn_key(map, index)); + scr_dyn_arr_push(value, iterator->kind == 3 ? scr_map_dyn_key(map, index) : scr_map_dyn_value(map, index)); + } else value = iterator->selection == 1 || iterator->kind == 3 + ? scr_map_dyn_key(map, index) : scr_map_dyn_value(map, index); } else if (items->kind == SCR_DYN_ARR) { value = scr_dyn_arr_at(items, iterator->index++); } else if (items->kind == SCR_DYN_BYTES) { @@ -2581,14 +2715,15 @@ static ScrDyn *scr_native_iterator_next(ScrNativeIterator *iterator) { return result; } -static ScrDyn *scr_native_iterator_next_checked(bool string_iterator) { +static ScrDyn *scr_native_iterator_next_checked(unsigned kind) { ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_ITERATOR && - ((ScrNativeIterator *)self->v.handle.ptr)->string_iterator == string_iterator) + ((ScrNativeIterator *)self->v.handle.ptr)->kind == kind) result = scr_native_iterator_next(self->v.handle.ptr); else { - const char *message = string_iterator + const char *message = kind == 3 ? "Method Set Iterator.prototype.next called on incompatible receiver" + : kind == 2 ? "Method Map Iterator.prototype.next called on incompatible receiver" : kind == 1 ? "Method String Iterator.prototype.next called on incompatible receiver" : "Method Array Iterator.prototype.next called on incompatible receiver"; scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); @@ -2599,17 +2734,29 @@ static ScrDyn *scr_native_iterator_next_checked(bool string_iterator) { static ScrDyn *scr_native_array_iterator_next_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; (void)args; (void)argc; - return scr_native_iterator_next_checked(false); + return scr_native_iterator_next_checked(0); } static ScrDyn *scr_native_string_iterator_next_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; (void)args; (void)argc; - return scr_native_iterator_next_checked(true); + return scr_native_iterator_next_checked(1); +} + +static ScrDyn *scr_native_map_iterator_next_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_native_iterator_next_checked(2); +} + +static ScrDyn *scr_native_set_iterator_next_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + return scr_native_iterator_next_checked(3); } static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { if (len != 4 || memcmp(key, "next", 4)) return NULL; - return ((ScrNativeIterator *)ptr)->string_iterator + unsigned kind = ((ScrNativeIterator *)ptr)->kind; + return kind == 3 ? scr_iterator_function(8, &scr_native_set_iterator_next_call, "next") + : kind == 2 ? scr_iterator_function(7, &scr_native_map_iterator_next_call, "next") : kind == 1 ? scr_iterator_function(5, &scr_native_string_iterator_next_call, "next") : scr_iterator_function(0, &scr_native_array_iterator_next_call, "next"); } @@ -2631,12 +2778,21 @@ static ScrDyn *scr_native_iterator_new(ScrDyn *source) { ScrNativeIterator *iterator = malloc(sizeof *iterator); if (!iterator) scr_json_oom(); *iterator = (ScrNativeIterator){ .rc = 1, .source = scr_dyn_retain(source), .index = 0, - .string_iterator = source->kind == SCR_DYN_STR }; + .kind = source->kind == SCR_DYN_STR ? 1 : source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_MAP ? 2 + : source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_SET ? 3 : 0, + .selection = source->kind == SCR_DYN_HANDLE && source->v.handle.tag == SCR_DYNH_MAP ? 2 : 0 }; + if (iterator->kind >= 2) scr_map_iter_enter(source->v.handle.ptr); ScrDyn *result = scr_dyn_new_handle(iterator, SCR_DYNH_ITERATOR); scr_native_iterator_release(iterator); return result; } +static ScrDyn *scr_native_collection_iterator(ScrDyn *source, unsigned selection) { + ScrDyn *result = scr_native_iterator_new(source); + ((ScrNativeIterator *)result->v.handle.ptr)->selection = selection; + return result; +} + static ScrDyn *scr_builtin_iterator_checked(int kind) { ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; @@ -2673,6 +2829,11 @@ static ScrDyn *scr_iterator_self_call(ScrClosure *closure, ScrDyn *const *args, static ScrDyn *scr_handle_iterator_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; (void)args; (void)argc; ScrDyn *self = scr_dyn_this_get(); + if (self->kind == SCR_DYN_HANDLE && (self->v.handle.tag == SCR_DYNH_MAP || self->v.handle.tag == SCR_DYNH_SET)) { + ScrDyn *result = scr_native_iterator_new(self); + scr_dyn_release(self); + return result; + } ScrDyn *items = scr_dyn_iter_pack(self, NULL); ScrDyn *result = items ? scr_native_iterator_new(items) : NULL; scr_dyn_release(items); @@ -2685,10 +2846,11 @@ ScrDyn *scr_dyn_array_values_function(void) { } static ScrDyn *scr_builtin_iterator_method(const ScrDyn *value) { + if (scr_dyn_generator(value)) return scr_iterator_function(4, &scr_iterator_self_call, "[Symbol.iterator]"); if (value->kind == SCR_DYN_ARR || (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value))) return scr_dyn_array_values_function(); if (value->kind == SCR_DYN_BYTES) return scr_iterator_function(2, &scr_builtin_bytes_iterator_call, "values"); if (value->kind == SCR_DYN_STR) return scr_iterator_function(3, &scr_builtin_string_iterator_call, "[Symbol.iterator]"); - if (value->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(value)->iter_pack) + if (value->kind == SCR_DYN_HANDLE && (value->v.handle.tag == SCR_DYNH_MAP || value->v.handle.tag == SCR_DYNH_SET || scr_dyn_handle_ops_of(value)->iter_pack)) return scr_iterator_function(6, &scr_handle_iterator_call, "[Symbol.iterator]"); return scr_iterator_function(4, &scr_iterator_self_call, "[Symbol.iterator]"); } @@ -2731,6 +2893,14 @@ ScrDyn *scr_dyn_iterator(const ScrDyn *source, const ScrStr *spell) { } static ScrDyn *scr_iterator_read(const ScrDyn *value, const char *key, size_t len) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (!view) return NULL; + ScrDyn *result = scr_iterator_read(view, key, len); + scr_dyn_release(view); + return result; + } + if (value->kind == SCR_DYN_PROXY) return scr_dyn_obj_read(value, key, len); if (value->kind == SCR_DYN_HANDLE) { ScrStr *name = scr_str_new(key, len); ScrDyn *result = scr_dyn_handle_key_get(value, name); @@ -2770,14 +2940,10 @@ ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *source) { numeric = false; } if (!numeric) { scr_dyn_release(out); return NULL; } - for (size_t i = 0; (double)i < floor(n); i++) { - char key[32]; - int size = snprintf(key, sizeof key, "%zu", i); - ScrDyn *value = scr_iterator_read(source, key, (size_t)size); - if (!value) { scr_dyn_release(out); return NULL; } - scr_dyn_arr_push(out, value); - } - ScrDyn *iterator = scr_native_iterator_new(out); + ScrDyn *iterator = scr_native_iterator_new((ScrDyn *)source); + ScrNativeIterator *state = iterator->v.handle.ptr; + state->array_like = true; + state->limit = isnan(n) || n < 0 ? 0 : floor(n); scr_dyn_release(out); return iterator; } @@ -2791,7 +2957,9 @@ ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *source) { } ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional) { - if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value)) { + if (scr_dyn_generator(value)) return scr_dyn_generator_symbol(value, key) + ? scr_iterator_function(4, &scr_iterator_self_call, "[Symbol.iterator]") : scr_dyn_retain(scr_dyn_undefined()); + if (value->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(value); ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_symbol_key_get(view, key, optional); scr_dyn_release(view); @@ -2892,7 +3060,12 @@ static bool scr_dyn_key_probe_computed(const ScrDyn *value, const ScrDyn *raw_ke scr_dyn_release(key); return out; } - if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_isl_is_array(value)) { + if (scr_dyn_generator(value)) { + bool found = mode == 0 && scr_dyn_generator_symbol(value, key); + scr_dyn_release(key); + return found; + } + if (value->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(value); bool out = !scr_exc_pending() && scr_dyn_key_probe_computed(view, key, mode); scr_dyn_release(view); @@ -2961,7 +3134,7 @@ void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *raw_key, bool stri * Refuse overrides until every typed call site observes the property table. */ static bool scr_dyn_fn_property_fence(const ScrStr *key) { static const char *const reserved[] = { - "call", "apply", "bind", "toString", "valueOf", "toLocaleString", + "call", "apply", "bind", "valueOf", "toLocaleString", "hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf", "constructor", "caller", "arguments", "__proto__", }; @@ -3484,6 +3657,29 @@ static SCR_TL ScrErrDynEnt *scr_errdyn_cache = NULL; static SCR_TL size_t scr_errdyn_n = 0, scr_errdyn_cap = 0; static SCR_TL bool scr_errdyn_teardown_registered = false; +typedef struct { + const ScrVt *vt; + ScrDyn *(*box)(void *); +} ScrErrorDynType; +static SCR_TL ScrErrorDynType *scr_error_dyn_types = NULL; +static SCR_TL size_t scr_error_dyn_type_count = 0; + +static void scr_error_dyn_types_drop(void) { + free(scr_error_dyn_types); + scr_error_dyn_types = NULL; + scr_error_dyn_type_count = 0; +} + +void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)) { + for (size_t i = 0; i < scr_error_dyn_type_count; i++) { + if (scr_error_dyn_types[i].vt == vt) return; + } + if (!scr_error_dyn_type_count) scr_atexit(scr_error_dyn_types_drop); + scr_error_dyn_types = realloc(scr_error_dyn_types, (scr_error_dyn_type_count + 1) * sizeof *scr_error_dyn_types); + if (!scr_error_dyn_types) scr_json_oom(); + scr_error_dyn_types[scr_error_dyn_type_count++] = (ScrErrorDynType){vt, box}; +} + static void scr_errdyn_teardown(void) { for (size_t i = 0; i < scr_errdyn_n; i++) { scr_error_release(scr_errdyn_cache[i].err); @@ -3494,14 +3690,31 @@ static void scr_errdyn_teardown(void) { scr_errdyn_n = scr_errdyn_cap = 0; } -ScrDyn *scr_dyn_from_error(const ScrError *e) { - for (size_t i = 0; i < scr_errdyn_n; i++) { - if (scr_errdyn_cache[i].err == e) return scr_dyn_retain(scr_errdyn_cache[i].dyn); - } +ScrDyn *scr_error_dyn_fields(const ScrError *e) { ScrDyn *d = scr_dyn_new_obj(); scr_dyn_obj_set(d, "%error", 6, scr_dyn_new_bool(true)); /* the checked-dynamic tree's error marker */ - scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); - scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); + if (e->name_present) { + scr_dyn_obj_set(d, "name", 4, scr_dyn_new_str(e->name)); + d->v.obj.entries[d->v.obj.len - 1].enumerable = e->name_enumerable; + } + if (!e->name_present || !e->message_present) { + ScrDyn *prototype = scr_dyn_new_obj(); + if (!e->name_present) { + scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(e->name)); + prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + } + if (!e->message_present) { + scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(e->message)); + prototype->v.obj.entries[prototype->v.obj.len - 1].enumerable = false; + } + scr_dyn_release(scr_dyn_set_prototype(d, prototype)); + scr_dyn_release(prototype); + } + if (e->message_present) { + scr_dyn_obj_set(d, "message", 7, scr_dyn_new_str(e->message)); + d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; + } + if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code)); if (e->error_cause) { scr_dyn_obj_set(d, "cause", 5, scr_dyn_retain(e->error_cause)); @@ -3515,6 +3728,21 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) { scr_dyn_obj_set(d, "cause", 5, scr_domex_cause((ScrError *)e)); } } + return d; +} + +ScrDyn *scr_dyn_from_error(const ScrError *e) { + for (size_t i = 0; i < scr_errdyn_n; i++) { + if (scr_errdyn_cache[i].err == e) return scr_dyn_retain(scr_errdyn_cache[i].dyn); + } + ScrDyn *d = NULL; + for (size_t i = 0; i < scr_error_dyn_type_count; i++) { + if (scr_error_dyn_types[i].vt == e->vt) { + d = scr_error_dyn_types[i].box((void *)e); + break; + } + } + if (!d) d = scr_error_dyn_fields(e); if (scr_errdyn_n == scr_errdyn_cap) { scr_errdyn_cap = scr_errdyn_cap ? scr_errdyn_cap * 2 : 8; scr_errdyn_cache = realloc(scr_errdyn_cache, scr_errdyn_cap * sizeof *scr_errdyn_cache); @@ -3543,6 +3771,54 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) { * its next boxing answers the same dyn node. The dyn node is borrowed. */ static void scr_error_cause_drop_impl(void *obj); +void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { + ScrDynEntry *name_entry = NULL; + for (size_t i = 0; i < view->v.obj.len; i++) { + ScrDynEntry *entry = &view->v.obj.entries[i]; + if (entry->key_len == 4 && memcmp(entry->key, "name", 4) == 0) { name_entry = entry; break; } + } + ScrDyn *name = name_entry ? name_entry->value : NULL; + if (name && name->kind != SCR_DYN_STR) { + scr_dyn_check_fail(NULL, "string", name); + return; + } + ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); + scr_str_release(e->name); + e->name = next_name; + e->name_present = name != NULL; + e->name_enumerable = name_entry && name_entry->enumerable; + ScrDyn *message = scr_dyn_obj_get(view, "message", 7); + if (message && message->kind != SCR_DYN_STR) { + scr_dyn_check_fail(NULL, "string", message); + return; + } + ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); + scr_str_release(e->message); + e->message = next_message; + e->message_present = message != NULL; + e->message_enumerable = false; + for (size_t i = 0; message && i < view->v.obj.len; i++) { + const ScrDynEntry *entry = &view->v.obj.entries[i]; + if (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0) e->message_enumerable = entry->enumerable; + } + ScrDyn *stack = scr_dyn_obj_get(view, "stack", 5); + if (stack && stack->kind == SCR_DYN_STR) { + ScrStr *replacement = scr_str_retain(stack->v.str); + scr_str_release(e->stack); + e->stack = replacement; + } + ScrDyn *cause = scr_dyn_obj_get(view, "cause", 5); + scr_error_install_cause_drop(&scr_error_cause_drop_impl); + ScrDyn *replacement = cause ? scr_dyn_retain(cause) : NULL; + scr_dyn_release(e->error_cause); + e->error_cause = replacement; + e->cause_enumerable = false; + for (size_t i = 0; cause && i < view->v.obj.len; i++) { + const ScrDynEntry *entry = &view->v.obj.entries[i]; + if (entry->key_len == 5 && memcmp(entry->key, "cause", 5) == 0) e->cause_enumerable = entry->enumerable; + } +} + ScrError *scr_error_from_dyn(const ScrDyn *d) { ScrError *hit = scr_errdyn_err_of(d); if (hit) return hit; @@ -3563,6 +3839,8 @@ ScrError *scr_error_from_dyn(const ScrDyn *d) { if (en && en->kind == SCR_DYN_STR) { scr_str_release(e->name); e->name = scr_str_retain(en->v.str); + e->name_present = true; + e->name_enumerable = true; } if (ec && ec->kind == SCR_DYN_STR) e->code = scr_str_retain(ec->v.str); ScrDyn *cause = scr_dyn_obj_get(d, "cause", 5); @@ -3608,11 +3886,20 @@ void scr_errdyn_put(ScrError *e, ScrDyn *d) { /* Error.cause has one live value even when the Error already crossed a * checked-value boundary. Reflective data-property edits update its slot. */ static void scr_error_sync_cause(ScrDyn *view, const ScrStr *key) { - if (key->len != 5 || memcmp(key->data, "cause", 5) != 0) return; + bool cause = key->len == 5 && memcmp(key->data, "cause", 5) == 0; + bool stack = key->len == 5 && memcmp(key->data, "stack", 5) == 0; + if (!cause && !stack) return; ScrDynEntry *entry = scr_dyn_entry(view, key); for (size_t i = 0; i < scr_errdyn_n; i++) { if (scr_errdyn_cache[i].dyn != view) continue; ScrError *e = scr_errdyn_cache[i].err; + if (stack) { + if (entry && entry->value->kind == SCR_DYN_STR) { + scr_str_release(e->stack); + e->stack = scr_str_retain(entry->value->v.str); + } + continue; + } if (e->vt == &scr_error_vts[SCR_ERR_DOMEX]) continue; ScrDyn *replacement = entry ? scr_dyn_retain(entry->value) : NULL; scr_error_install_cause_drop(&scr_error_cause_drop_impl); @@ -3778,7 +4065,7 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return scr_str_new("", 0); } - return scr_dyn_to_string(d, NULL); + return scr_dyn_to_string_method(d, NULL, what); } /* JS String() over the dyn kind — the WebIDL ToString the web globals @@ -4200,12 +4487,10 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) { return out; } if (v->kind == SCR_DYN_OBJ) { - for (const ScrDyn *current = v; current && current->kind == SCR_DYN_OBJ; current = current->prototype) { - if (scr_dyn_obj_get(current, key->data, key->len)) return true; - } - return false; + return scr_dyn_property_owner(v, key->data, key->len) != NULL; } if (v->kind == SCR_DYN_FUNC) { + if (scr_dyn_property_owner(v, key->data, key->len)) return true; ScrDyn *descriptor = scr_dyn_own_descriptor(v, key); bool present = descriptor && descriptor->kind != SCR_DYN_UNDEF; scr_dyn_release(descriptor); @@ -5311,6 +5596,7 @@ static ScrDyn *scr_json_revive(ScrDyn *holder, const ScrStr *key, ScrDyn *scr_json_parse_reviver(ScrStr *text, const ScrDyn *reviver) { ScrDyn *parsed = scr_json_parse(text); if (!parsed) return NULL; /* No callback runs on invalid input. */ + if (reviver->kind != SCR_DYN_FUNC) return parsed; ScrDyn *holder = scr_dyn_new_obj(); scr_dyn_obj_set(holder, "", 0, parsed); ScrStr *key = scr_str_new("", 0); @@ -5580,6 +5866,8 @@ static ScrDyn *scr_function_constructor(uint32_t kind) { /* Keyed read on a FUNC node (see scr_runtime.h): own props first, then * the function-instance built-ins name/length. +1 or NULL. */ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { + if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !d->v.fn.clo->props) + d->v.fn.clo->props = scr_box_retain(d->v.fn.class_obj->static_data); if (!d->v.fn.class_obj && key_len == 9 && !memcmp(key, "prototype", 9) && !d->v.fn.clo->props) { if (d->v.fn.clo->function_kind & 1) { static const char message[] = "Native generator function prototypes have no lowering"; @@ -5620,6 +5908,7 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) { if (key_len == 6 && memcmp(key, "length", 6) == 0) { return scr_dyn_new_num((double)d->v.fn.arity); } + if (key_len == 5 && memcmp(key, "apply", 5) == 0) return scr_dyn_function_apply(); if (d->v.fn.class_obj) { static const char message[] = "scriptc: class properties through unknown other than name and length are not supported"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -5814,6 +6103,16 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) { static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver) { for (const ScrDyn *current = d; current; current = current->prototype) { + if (current == scr_builtin_object_prototype) { + scr_dyn_class_reflection_fence(current); + return NULL; + } + if (current->kind == SCR_DYN_FUNC) { + ScrDyn *table = scr_dyn_fn_properties(current); + ScrDyn *result = scr_dyn_obj_read_receiver(table, key, key_len, receiver); + scr_dyn_release(table); + return result; + } if (current->kind == SCR_DYN_ARR) { if (key_len == 6 && memcmp(key, "length", 6) == 0) return scr_dyn_new_num((double)current->v.arr.len); double index; @@ -5862,7 +6161,77 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_ return scr_dyn_retain(scr_dyn_undefined()); } +static SCR_TL ScrDyn *scr_function_apply_value; + +static void scr_function_apply_cleanup(void) { + scr_dyn_release(scr_function_apply_value); + scr_function_apply_value = NULL; +} + +ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what) { + ScrDyn *list = scr_dyn_new_arr(); + if (arguments && arguments->kind != SCR_DYN_UNDEF && arguments->kind != SCR_DYN_NULL) { + if (!scr_iterator_object(arguments)) { + static const char message[] = "CreateListFromArrayLike called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } else { + ScrDyn *length = scr_iterator_read(arguments, "length", 6); + double count = 0; + bool ok = length && scr_dyn_number_coerce_js(length, &count); + scr_dyn_release(length); + if (ok && count > 1000000) { + static const char message[] = "Function.prototype.apply exceeds the native argument limit"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else if (ok) for (size_t i = 0; (double)i < floor(count); i++) { + char key[32]; + int size = snprintf(key, sizeof key, "%zu", i); + ScrDyn *value = scr_iterator_read(arguments, key, (size_t)size); + if (!value) break; + scr_dyn_arr_push(list, value); + } + } + } + ScrDyn *result = NULL; + if (!scr_exc_pending()) { + scr_dyn_this_push_dyn(receiver); + result = scr_dyn_call(target, list->v.arr.items, list->v.arr.len, what); + scr_dyn_this_pop(); + } + scr_dyn_release(list); + return result; +} + +static ScrDyn *scr_function_apply_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + ScrDyn *receiver = scr_dyn_this_get(); + if (receiver->kind != SCR_DYN_FUNC) { + scr_dyn_release(receiver); + static const char message[] = "Function.prototype.apply was called on a non-callable value"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrDyn *result = scr_dyn_apply_array_like(receiver, argc ? args[0] : scr_dyn_undefined(), + argc > 1 ? args[1] : scr_dyn_undefined(), "Function.prototype.apply"); + scr_dyn_release(receiver); + return result; +} + +ScrDyn *scr_dyn_function_apply(void) { + if (!scr_function_apply_value) { + scr_function_apply_value = scr_dyn_new_func(scr_closure_new(NULL, 0), scr_function_apply_call, + 2, "native:Function.prototype.apply", "apply"); + scr_atexit(scr_function_apply_cleanup); + } + return scr_dyn_retain(scr_function_apply_value); +} + ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len) { + if (d->kind == SCR_DYN_PROXY) { + ScrStr *name = scr_str_new(key, key_len); + ScrDyn *value = scr_dyn_proxy_get(d, name); + scr_str_release(name); + return value; + } return scr_dyn_obj_read_receiver(d, key, key_len, d); } @@ -5933,6 +6302,13 @@ ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) { } ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } + ScrDyn *result = scr_dyn_get_own_property_names(view); + scr_dyn_release(view); + return result; + } if (value->kind == SCR_DYN_FUNC) { if (scr_dyn_class_reflection_fence(value)) return NULL; ScrDyn *table = scr_dyn_fn_properties(value); @@ -5974,6 +6350,13 @@ ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value) { } ScrDyn *scr_dyn_get_own_property_symbols(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } + ScrDyn *result = scr_dyn_get_own_property_symbols(view); + scr_dyn_release(view); + return result; + } if (value->kind == SCR_DYN_NULL || value->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); @@ -5989,13 +6372,20 @@ ScrDyn *scr_dyn_get_own_property_symbols(const ScrDyn *value) { } ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object) { + if (object->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(object); + if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } + ScrDyn *result = scr_dyn_get_own_property_descriptors(view); + scr_dyn_release(view); + return result; + } if (scr_dyn_class_reflection_fence(object)) return NULL; if (object->kind == SCR_DYN_UNDEF || object->kind == SCR_DYN_NULL) { static const char message[] = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if (object->kind == SCR_DYN_PROXY || object->kind == SCR_DYN_TYPED_REF || + if (object->kind == SCR_DYN_PROXY || object->kind == SCR_DYN_JSVAL || object->kind == SCR_DYN_HANDLE) { static const char message[] = "Object.getOwnPropertyDescriptors on this value is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); @@ -6026,6 +6416,21 @@ ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object) { return result; } +ScrDyn *scr_dyn_own_keys(const ScrDyn *value) { + if (!scr_dyn_to_primitive_result_is_object(value)) { + static const char message[] = "Reflect.ownKeys called on non-object"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + return NULL; + } + ScrDyn *names = scr_dyn_get_own_property_names(value); + if (!names) return NULL; + ScrDyn *symbols = scr_dyn_get_own_property_symbols(value); + if (!symbols) { scr_dyn_release(names); return NULL; } + for (size_t i = 0; i < symbols->v.arr.len; i++) scr_dyn_arr_push(names, scr_dyn_retain(symbols->v.arr.items[i])); + scr_dyn_release(symbols); + return names; +} + static void scr_dyn_objwalk_push(ScrDyn *out, ScrObjWalk mode, const char *key, size_t key_len, ScrDyn *value /* borrowed */) { switch (mode) { @@ -6710,6 +7115,7 @@ void scr_error_init_options(void *obj, int kind, const ScrDyn *message, const Sc return; } scr_error_init(obj, kind, text); + ((ScrError *)obj)->message_present = message && message->kind != SCR_DYN_UNDEF; scr_str_release(text); ScrDyn *view = options && options->kind == SCR_DYN_TYPED_REF ? scr_dyn_typed_ref_materialize(options) : NULL; @@ -6742,7 +7148,15 @@ ScrError *scr_error_new_options(int kind, const ScrDyn *message, const ScrDyn *o ScrDyn *scr_error_cause(ScrError *e) { for (size_t i = 0; i < scr_errdyn_n; i++) { - if (scr_errdyn_cache[i].err == e) return scr_dyn_obj_read(scr_errdyn_cache[i].dyn, "cause", 5); + if (scr_errdyn_cache[i].err != e) continue; + ScrDyn *value = scr_errdyn_cache[i].dyn; + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(value); + ScrDyn *cause = scr_dyn_obj_read(view, "cause", 5); + scr_dyn_release(view); + return cause; + } + return scr_dyn_obj_read(value, "cause", 5); } return e->error_cause ? scr_dyn_retain(e->error_cause) : scr_dyn_undefined(); } @@ -7470,7 +7884,12 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) { if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP && key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO) return -1; *ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1; - case SCR_DYN_TYPED_REF: case SCR_DYN_PROMISE: case SCR_DYN_JSVAL: + case SCR_DYN_TYPED_REF: + if (!key->v.typed_ref.traced) return -1; + *ptr = key->v.typed_ref.ptr; *kind = 32; return 1; + case SCR_DYN_PROMISE: + *ptr = key->v.promise; *kind = 33; return 1; + case SCR_DYN_JSVAL: return -1; default: return 0; } @@ -7789,12 +8208,15 @@ static ScrDyn *scr_native_set_invoke(void *ptr, ScrDyn *self, const char *method ScrMap *set = ptr; ScrDyn *value = argc ? args[0] : scr_dyn_undefined(); if (strcmp(method, "add") == 0) { - scr_map_set_ref_f64(set, value, 1); + scr_map_dyn_set(set, value, NULL, true); + if (scr_exc_pending()) return NULL; return scr_dyn_retain(self); } - if (strcmp(method, "has") == 0) return scr_dyn_new_bool(scr_map_has_ref(set, value)); - if (strcmp(method, "delete") == 0) return scr_dyn_new_bool(scr_map_delete_ref(set, value)); + if (strcmp(method, "has") == 0) return scr_dyn_new_bool(scr_map_dyn_has(set, value, false)); + if (strcmp(method, "delete") == 0) return scr_dyn_new_bool(scr_map_dyn_has(set, value, true)); if (strcmp(method, "clear") == 0) { scr_map_clear(set); return scr_dyn_retain(scr_dyn_undefined()); } + if (strcmp(method, "values") == 0 || strcmp(method, "keys") == 0) return scr_native_collection_iterator(self, 0); + if (strcmp(method, "entries") == 0) return scr_native_collection_iterator(self, 2); if (strcmp(method, "forEach") == 0) { if (value->kind != SCR_DYN_FUNC && !(value->kind == SCR_DYN_JSVAL && scr_dyn_isl_typeof_is(value, "function"))) { @@ -7817,7 +8239,7 @@ static ScrDyn *scr_native_set_invoke(void *ptr, ScrDyn *self, const char *method scr_map_iter_enter(set); for (double i = 0; i < scr_map_iter_count(set); i++) { if (!scr_map_iter_live(set, i)) continue; - ScrDyn *entry = scr_map_iter_key_ref(set, i); + ScrDyn *entry = scr_map_dyn_key(set, i); ScrDyn *callback_args[] = {entry, entry, self}; scr_dyn_this_push_dyn(this_arg); ScrDyn *result = scr_dyn_call(value, callback_args, 3, "callback"); @@ -7887,16 +8309,44 @@ static ScrDyn *scr_native_map_invoke(void *ptr, ScrDyn *self, const char *method ScrMap *map = ptr; ScrDyn *key = argc ? args[0] : scr_dyn_undefined(); if (strcmp(method, "set") == 0) { - scr_map_set_ref_ref(map, key, scr_dyn_retain(argc > 1 ? args[1] : scr_dyn_undefined())); + scr_map_dyn_set(map, key, argc > 1 ? args[1] : scr_dyn_undefined(), false); + if (scr_exc_pending()) return NULL; return scr_dyn_retain(self); } if (strcmp(method, "get") == 0) { - ScrDyn *value = scr_map_get_ref_ref(map, key); - return value ? value : scr_dyn_retain(scr_dyn_undefined()); + return scr_map_dyn_get(map, key); } - if (strcmp(method, "has") == 0) return scr_dyn_new_bool(scr_map_has_ref(map, key)); - if (strcmp(method, "delete") == 0) return scr_dyn_new_bool(scr_map_delete_ref(map, key)); + if (strcmp(method, "has") == 0) return scr_dyn_new_bool(scr_map_dyn_has(map, key, false)); + if (strcmp(method, "delete") == 0) return scr_dyn_new_bool(scr_map_dyn_has(map, key, true)); if (strcmp(method, "clear") == 0) { scr_map_clear(map); return scr_dyn_retain(scr_dyn_undefined()); } + if (strcmp(method, "values") == 0) return scr_native_collection_iterator(self, 0); + if (strcmp(method, "keys") == 0) return scr_native_collection_iterator(self, 1); + if (strcmp(method, "entries") == 0) return scr_native_collection_iterator(self, 2); + if (strcmp(method, "forEach") == 0) { + if (key->kind != SCR_DYN_FUNC && !(key->kind == SCR_DYN_JSVAL && scr_dyn_isl_typeof_is(key, "function"))) { + /* Validate even when there are no entries to visit. */ + ScrDyn *result = scr_dyn_call(key, NULL, 0, "callback"); + scr_dyn_release(result); + return NULL; + } + ScrDyn *this_arg = argc > 1 ? args[1] : scr_dyn_undefined(); + scr_map_iter_enter(map); + for (double i = 0; i < scr_map_iter_count(map); i++) { + if (!scr_map_iter_live(map, i)) continue; + ScrDyn *entry_key = scr_map_dyn_key(map, i); + ScrDyn *value = scr_map_dyn_value(map, i); + ScrDyn *callback_args[] = {value, entry_key, self}; + scr_dyn_this_push_dyn(this_arg); + ScrDyn *result = scr_dyn_call(key, callback_args, 3, "callback"); + scr_dyn_this_pop(); + scr_dyn_release(entry_key); + scr_dyn_release(value); + scr_dyn_release(result); + if (scr_exc_pending()) break; + } + scr_map_iter_exit(map); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); + } (void)what; static const char message[] = "Native Map method has no lowering"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); @@ -7910,8 +8360,8 @@ static ScrDyn *scr_native_map_pack(void *ptr) { for (double i = 0; i < scr_map_iter_count(map); i++) { if (!scr_map_iter_live(map, i)) continue; ScrDyn *pair = scr_dyn_new_arr(); - scr_dyn_arr_push(pair, scr_map_iter_key_ref(map, i)); - scr_dyn_arr_push(pair, scr_map_iter_val_ref(map, i)); + scr_dyn_arr_push(pair, scr_map_dyn_key(map, i)); + scr_dyn_arr_push(pair, scr_map_dyn_value(map, i)); scr_dyn_arr_push(entries, pair); } scr_map_iter_exit(map); @@ -7936,7 +8386,37 @@ ScrMap *scr_dyn_native_map_check(const ScrDyn *value, const ScrDynPath *path) { return scr_map_retain(value->v.handle.ptr); } +bool scr_dyn_native_collection_is(const ScrDyn *value, int map, const char *type) { + if (!(map ? scr_dyn_native_map_is(value) : scr_dyn_native_set_is(value))) return false; + const ScrMap *storage = value->v.handle.ptr; + return type ? storage->dyn_ops && strcmp(storage->dyn_ops->type, type) == 0 : !storage->dyn_ops; +} + +ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char *type, const ScrDynPath *path) { + if (!scr_dyn_native_collection_is(value, map, type)) { + scr_dyn_check_fail(path, type ? type : map ? "Map" : "Set", value); + return NULL; + } + return scr_map_retain(value->v.handle.ptr); +} + ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { + if (d->kind == SCR_DYN_FUNC) { + ScrDyn *member = scr_dyn_fn_get(d, "toString", 8); + if (scr_exc_pending()) { scr_dyn_release(member); return NULL; } + if (member && member->kind != SCR_DYN_UNDEF) { + scr_dyn_this_push_dyn(d); + ScrDyn *value = scr_dyn_call(member, NULL, 0, what->data); + scr_dyn_this_pop(); + scr_dyn_release(member); + if (!value) return NULL; + ScrStr *result = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str) : NULL; + if (!result) scr_dyn_check_fail(NULL, "string", value); + scr_dyn_release(value); + return result; + } + scr_dyn_release(member); + } if (d->kind == SCR_DYN_OBJ && !scr_dyn_property_owner(d, "toString", 8) && !scr_dyn_has_object_prototype(d)) { ScrJsonBuf b; scr_jb_init(&b); diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index a22dbd5a7..b7abb0161 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -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 */ } diff --git a/packages/runtime/src/scr_object.c b/packages/runtime/src/scr_object.c index 225099d8a..dac981530 100644 --- a/packages/runtime/src/scr_object.c +++ b/packages/runtime/src/scr_object.c @@ -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) { diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 845a85396..2792b480e 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -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 " 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 * " 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); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index d4997e267..36415c57b 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -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; } diff --git a/scripts/sandbox-test.mjs b/scripts/sandbox-test.mjs index d47ba30bc..c1936b6c5 100644 --- a/scripts/sandbox-test.mjs +++ b/scripts/sandbox-test.mjs @@ -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", diff --git a/tests/corpus/2038-evolving-array-js.cjs b/tests/corpus/2038-evolving-array-js.cjs index cf114f6c8..1390e0e54 100644 --- a/tests/corpus/2038-evolving-array-js.cjs +++ b/tests/corpus/2038-evolving-array-js.cjs @@ -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))); diff --git a/tests/corpus/arguments-arrow-capture.mjs b/tests/corpus/arguments-arrow-capture.mjs new file mode 100644 index 000000000..1b3c711a8 --- /dev/null +++ b/tests/corpus/arguments-arrow-capture.mjs @@ -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")()); diff --git a/tests/corpus/array-checked-callback-results.mjs b/tests/corpus/array-checked-callback-results.mjs new file mode 100644 index 000000000..90fd3d5d5 --- /dev/null +++ b/tests/corpus/array-checked-callback-results.mjs @@ -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("|")); diff --git a/tests/corpus/array-from-value.mjs b/tests/corpus/array-from-value.mjs new file mode 100644 index 000000000..e7a629827 --- /dev/null +++ b/tests/corpus/array-from-value.mjs @@ -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); } diff --git a/tests/corpus/builtin-prototype-method-values.mjs b/tests/corpus/builtin-prototype-method-values.mjs new file mode 100644 index 000000000..f366131a9 --- /dev/null +++ b/tests/corpus/builtin-prototype-method-values.mjs @@ -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)); diff --git a/tests/corpus/catch-capture.ts b/tests/corpus/catch-capture.ts new file mode 100644 index 000000000..4f5c74816 --- /dev/null +++ b/tests/corpus/catch-capture.ts @@ -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(); diff --git a/tests/corpus/checked-array-assignment.ts b/tests/corpus/checked-array-assignment.ts new file mode 100644 index 000000000..c05f2bb7b --- /dev/null +++ b/tests/corpus/checked-array-assignment.ts @@ -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)); diff --git a/tests/corpus/checked-array-flat/main.ts b/tests/corpus/checked-array-flat/main.ts new file mode 100644 index 000000000..783020fe9 --- /dev/null +++ b/tests/corpus/checked-array-flat/main.ts @@ -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(); +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))); diff --git a/tests/corpus/checked-array-flat/operations.mjs b/tests/corpus/checked-array-flat/operations.mjs new file mode 100644 index 000000000..9c92b66db --- /dev/null +++ b/tests/corpus/checked-array-flat/operations.mjs @@ -0,0 +1,3 @@ +export function flatten(values, depth) { + return [values.get("outer"), values.get("record"), 5].flat(depth); +} diff --git a/tests/corpus/checked-callback-options.ts b/tests/corpus/checked-callback-options.ts new file mode 100644 index 000000000..bcf65283c --- /dev/null +++ b/tests/corpus/checked-callback-options.ts @@ -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); diff --git a/tests/corpus/checked-class-descriptors.mjs b/tests/corpus/checked-class-descriptors.mjs new file mode 100644 index 000000000..c8128cdbd --- /dev/null +++ b/tests/corpus/checked-class-descriptors.mjs @@ -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]); diff --git a/tests/corpus/checked-class-factory.mjs b/tests/corpus/checked-class-factory.mjs new file mode 100644 index 000000000..5c4276426 --- /dev/null +++ b/tests/corpus/checked-class-factory.mjs @@ -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); diff --git a/tests/corpus/checked-collection-iterators.ts b/tests/corpus/checked-collection-iterators.ts new file mode 100644 index 000000000..18311fae6 --- /dev/null +++ b/tests/corpus/checked-collection-iterators.ts @@ -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([["a", 1], ["b", 2]]), new Set([1, 2])); diff --git a/tests/corpus/checked-collection-symbol-methods.ts b/tests/corpus/checked-collection-symbol-methods.ts new file mode 100644 index 000000000..1f9c1bd10 --- /dev/null +++ b/tests/corpus/checked-collection-symbol-methods.ts @@ -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]); diff --git a/tests/corpus/checked-inherited-fields.ts b/tests/corpus/checked-inherited-fields.ts new file mode 100644 index 000000000..41b6069fd --- /dev/null +++ b/tests/corpus/checked-inherited-fields.ts @@ -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 }); diff --git a/tests/corpus/checked-optional-calls.mjs b/tests/corpus/checked-optional-calls.mjs new file mode 100644 index 000000000..de884108e --- /dev/null +++ b/tests/corpus/checked-optional-calls.mjs @@ -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)); diff --git a/tests/corpus/checked-promise-recovery.ts b/tests/corpus/checked-promise-recovery.ts new file mode 100644 index 000000000..08d3c112f --- /dev/null +++ b/tests/corpus/checked-promise-recovery.ts @@ -0,0 +1,12 @@ +export {}; +const source: unknown = Promise.resolve(42); +const recovered = source as Promise; +console.log(recovered === (source as Promise), await recovered); +function optional(value: unknown): Promise | undefined { + return value as Promise | 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); } +catch (error) { console.log(error); } diff --git a/tests/corpus/checked-proxy-callback/main.ts b/tests/corpus/checked-proxy-callback/main.ts new file mode 100644 index 000000000..670bd9bc4 --- /dev/null +++ b/tests/corpus/checked-proxy-callback/main.ts @@ -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(",")); diff --git a/tests/corpus/checked-proxy-callback/operations.mjs b/tests/corpus/checked-proxy-callback/operations.mjs new file mode 100644 index 000000000..27ba3e739 --- /dev/null +++ b/tests/corpus/checked-proxy-callback/operations.mjs @@ -0,0 +1,3 @@ +export function makeTarget() { + return Object.assign(Object.create({ kind: "number" }), { value: 21 }); +} diff --git a/tests/corpus/checked-record-constructor.ts b/tests/corpus/checked-record-constructor.ts new file mode 100644 index 000000000..95d04ed20 --- /dev/null +++ b/tests/corpus/checked-record-constructor.ts @@ -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); } diff --git a/tests/corpus/checked-symbol-array.ts b/tests/corpus/checked-symbol-array.ts new file mode 100644 index 000000000..ac861dadb --- /dev/null +++ b/tests/corpus/checked-symbol-array.ts @@ -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); diff --git a/tests/corpus/class-curried-factory.mjs b/tests/corpus/class-curried-factory.mjs new file mode 100644 index 000000000..d9a9230fe --- /dev/null +++ b/tests/corpus/class-curried-factory.mjs @@ -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")]); diff --git a/tests/corpus/class-factory-inherited-statics.mjs b/tests/corpus/class-factory-inherited-statics.mjs new file mode 100644 index 000000000..76e6b99d0 --- /dev/null +++ b/tests/corpus/class-factory-inherited-statics.mjs @@ -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); diff --git a/tests/corpus/class-factory-parameter-base.mjs b/tests/corpus/class-factory-parameter-base.mjs new file mode 100644 index 000000000..1a4713a6b --- /dev/null +++ b/tests/corpus/class-factory-parameter-base.mjs @@ -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); diff --git a/tests/corpus/class-factory-record-base.mjs b/tests/corpus/class-factory-record-base.mjs new file mode 100644 index 000000000..e2bf7bef4 --- /dev/null +++ b/tests/corpus/class-factory-record-base.mjs @@ -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")); diff --git a/tests/corpus/class-factory-recursive-statics.mjs b/tests/corpus/class-factory-recursive-statics.mjs new file mode 100644 index 000000000..cd88e6dce --- /dev/null +++ b/tests/corpus/class-factory-recursive-statics.mjs @@ -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); diff --git a/tests/corpus/class-factory-static-members.mjs b/tests/corpus/class-factory-static-members.mjs new file mode 100644 index 000000000..0c2c2b25a --- /dev/null +++ b/tests/corpus/class-factory-static-members.mjs @@ -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)); diff --git a/tests/corpus/class-json-checked.ts b/tests/corpus/class-json-checked.ts new file mode 100644 index 000000000..98dcf5f83 --- /dev/null +++ b/tests/corpus/class-json-checked.ts @@ -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)); diff --git a/tests/corpus/class-local-function-base.mjs b/tests/corpus/class-local-function-base.mjs new file mode 100644 index 000000000..d516e4294 --- /dev/null +++ b/tests/corpus/class-local-function-base.mjs @@ -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); diff --git a/tests/corpus/class-prototype-descriptor-copy.mjs b/tests/corpus/class-prototype-descriptor-copy.mjs new file mode 100644 index 000000000..aad8e8463 --- /dev/null +++ b/tests/corpus/class-prototype-descriptor-copy.mjs @@ -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()); diff --git a/tests/corpus/class-specialized-computed-base.mjs b/tests/corpus/class-specialized-computed-base.mjs new file mode 100644 index 000000000..63f37db01 --- /dev/null +++ b/tests/corpus/class-specialized-computed-base.mjs @@ -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()); diff --git a/tests/corpus/class-super-surplus-spread.mjs b/tests/corpus/class-super-surplus-spread.mjs new file mode 100644 index 000000000..23b5d2a31 --- /dev/null +++ b/tests/corpus/class-super-surplus-spread.mjs @@ -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); } diff --git a/tests/corpus/class-symbol-accessors.ts b/tests/corpus/class-symbol-accessors.ts new file mode 100644 index 000000000..415e66f80 --- /dev/null +++ b/tests/corpus/class-symbol-accessors.ts @@ -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()); diff --git a/tests/corpus/class-symbol-cache.mjs b/tests/corpus/class-symbol-cache.mjs new file mode 100644 index 000000000..d993931a2 --- /dev/null +++ b/tests/corpus/class-symbol-cache.mjs @@ -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); diff --git a/tests/corpus/conditional-tuple-destructuring.ts b/tests/corpus/conditional-tuple-destructuring.ts new file mode 100644 index 000000000..f651e9b41 --- /dev/null +++ b/tests/corpus/conditional-tuple-destructuring.ts @@ -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(",")); diff --git a/tests/corpus/error-inherited-message.mjs b/tests/corpus/error-inherited-message.mjs new file mode 100644 index 000000000..fd792dba5 --- /dev/null +++ b/tests/corpus/error-inherited-message.mjs @@ -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); diff --git a/tests/corpus/error-inherited-name.mjs b/tests/corpus/error-inherited-name.mjs new file mode 100644 index 000000000..75a6e5298 --- /dev/null +++ b/tests/corpus/error-inherited-name.mjs @@ -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")); diff --git a/tests/corpus/error-property-writes.mjs b/tests/corpus/error-property-writes.mjs new file mode 100644 index 000000000..70ceb1304 --- /dev/null +++ b/tests/corpus/error-property-writes.mjs @@ -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"); diff --git a/tests/corpus/error-prototype-tostring.js b/tests/corpus/error-prototype-tostring.js index 8ad785421..a5b7a52ed 100644 --- a/tests/corpus/error-prototype-tostring.js +++ b/tests/corpus/error-prototype-tostring.js @@ -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)); diff --git a/tests/corpus/function-apply-value.mjs b/tests/corpus/function-apply-value.mjs new file mode 100644 index 000000000..574f4f4fa --- /dev/null +++ b/tests/corpus/function-apply-value.mjs @@ -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)); diff --git a/tests/corpus/function-constructor-global.mjs b/tests/corpus/function-constructor-global.mjs new file mode 100644 index 000000000..78ed3c99e --- /dev/null +++ b/tests/corpus/function-constructor-global.mjs @@ -0,0 +1,9 @@ +const generatorConstructor = function* () {}.constructor; +const asyncConstructor = async function () {}.constructor; +function kind(value) { + return value.constructor === generatorConstructor ? "generator" + : value.constructor === asyncConstructor ? "async" : "ordinary"; +} +console.log(kind(function* () { yield 1; })); +console.log(kind(async function () { return 2; })); +console.log(kind(() => 3)); diff --git a/tests/corpus/function-custom-to-string.mjs b/tests/corpus/function-custom-to-string.mjs new file mode 100644 index 000000000..e19474776 --- /dev/null +++ b/tests/corpus/function-custom-to-string.mjs @@ -0,0 +1,8 @@ +function render(value) { return value.toString(); } +function first() {} +function second() {} +Object.assign(first, { toString() { return this === first ? "first" : "wrong"; } }); +Object.assign(second, { toString() { return "second"; } }); +console.log(render(first), render(second)); +Object.assign(first, { toString() { return "changed"; } }); +console.log(render(first)); diff --git a/tests/corpus/generator-checked-protocol.ts b/tests/corpus/generator-checked-protocol.ts new file mode 100644 index 000000000..2a5ee5a07 --- /dev/null +++ b/tests/corpus/generator-checked-protocol.ts @@ -0,0 +1,21 @@ +async function* values() { + try { yield 20; yield 22; } + finally { console.log("closed"); } +} +function iterator(value: any) { return value[Symbol.asyncIterator](); } +async function run(value: any) { + const stream = iterator(value); + console.log(stream === value, typeof stream.next, typeof stream.return); + console.log(JSON.stringify(await stream.next())); + console.log(JSON.stringify(await stream.return())); + console.log(JSON.stringify(await stream.next())); +} +run(values()); +async function collect(value: any) { + const stream = iterator(value); + const next = async () => stream.next(); + console.log(JSON.stringify(await next())); + console.log(JSON.stringify(await next())); + console.log(JSON.stringify(await next())); +} +collect(values()); diff --git a/tests/corpus/generator-checked-return.js b/tests/corpus/generator-checked-return.js new file mode 100644 index 000000000..318c6e5e3 --- /dev/null +++ b/tests/corpus/generator-checked-return.js @@ -0,0 +1,12 @@ +function* returned(value) { + yield "ready"; + return value; +} +function* delegated(value) { + return yield* returned(value); +} +for (const value of [42, "world", { answer: 42 }]) { + const iterator = delegated(value); + console.log(JSON.stringify(iterator.next())); + console.log(JSON.stringify(iterator.next())); +} diff --git a/tests/corpus/generator-default-return-pattern.ts b/tests/corpus/generator-default-return-pattern.ts new file mode 100644 index 000000000..358e18e95 --- /dev/null +++ b/tests/corpus/generator-default-return-pattern.ts @@ -0,0 +1,17 @@ +function* records(): Generator<{ name: string; value: number }> { + yield { name: "first", value: 21 }; + yield { name: "second", value: 42 }; + return "done"; +} +for (const { name, value } of records()) console.log(name, value); +const iterator = records(); +console.log(iterator.next().value, iterator.next().value, iterator.next().value); + +async function* pairs(): AsyncGenerator<[string, number]> { + yield ["pair", 7]; + return true; +} +async function run() { + for await (const [name, value] of pairs()) console.log(name, value); +} +run(); diff --git a/tests/corpus/inferred-array-assignment-values.mjs b/tests/corpus/inferred-array-assignment-values.mjs new file mode 100644 index 000000000..e9e452812 --- /dev/null +++ b/tests/corpus/inferred-array-assignment-values.mjs @@ -0,0 +1,15 @@ +const values = new Array(2); +const alias = values; +const write = (index, value) => values[index] = value; +console.log(write(0, 42), alias[0]); +console.log(write("1", "end"), JSON.stringify(values)); + +const numbers = [0, 0]; +const setNumber = (index, value) => numbers[index] = value; +console.log(setNumber(0, 9), setNumber("1", 12), numbers.join(",")); + +let trace = ""; +function receiver() { trace += "receiver "; return values; } +function key() { trace += "key "; return 0; } +function value() { trace += "value "; return 7; } +console.log(receiver()[key()] = value(), values[0], trace); diff --git a/tests/corpus/inferred-class-default.mjs b/tests/corpus/inferred-class-default.mjs new file mode 100644 index 000000000..3363dde67 --- /dev/null +++ b/tests/corpus/inferred-class-default.mjs @@ -0,0 +1,10 @@ +class Empty { + kind = "empty"; +} +class Value { + kind = "value"; + constructor(value) { this.value = value; } +} +function read(value = new Empty()) { return value.kind; } +function invoke(fn, value) { return fn(value); } +console.log(read(), invoke(read, new Value(42)), invoke(read, undefined)); diff --git a/tests/corpus/inferred-compound-values.mjs b/tests/corpus/inferred-compound-values.mjs new file mode 100644 index 000000000..350bd2352 --- /dev/null +++ b/tests/corpus/inferred-compound-values.mjs @@ -0,0 +1,15 @@ +function append(value, suffix) { + let result = value; + result += suffix; + return result; +} +console.log(append("hello", ", world"), append(1, "2"), append(1n, 2n)); +function compute(value, rhs) { + let result = value; + result *= rhs; + return result; +} +console.log(compute("6", 7), compute(6n, 7n)); +let calls = 0; +const value = { valueOf() { calls++; return 4; } }; +console.log(append(value, 2), calls); diff --git a/tests/corpus/inferred-conditional-class.mjs b/tests/corpus/inferred-conditional-class.mjs new file mode 100644 index 000000000..8245c40f1 --- /dev/null +++ b/tests/corpus/inferred-conditional-class.mjs @@ -0,0 +1,16 @@ +class First { value = 1; } +class Second { value = 2; } +const fallback = new First(); +class State { + previous = undefined; + next(value) { + const previous = this.previous === undefined ? fallback : this.previous; + this.previous = value; + return previous; + } +} +const state = new State(); +const next = new Second(); +console.log(state.next(next) === fallback); +console.log(state.next(fallback) === next); +console.log(state.next(next).value); diff --git a/tests/corpus/inferred-error-fields.js b/tests/corpus/inferred-error-fields.js new file mode 100644 index 000000000..fc9d7b35a --- /dev/null +++ b/tests/corpus/inferred-error-fields.js @@ -0,0 +1,8 @@ +function makeError(message, options) { + const error = new Error(message, options); + if (error.message === "") error.message = "fallback"; + error.name = "ExampleError"; + return error; +} +const error = makeError("", { cause: "reason" }); +console.log(error.name, error.message, error.cause); diff --git a/tests/corpus/inferred-map-patterns.mjs b/tests/corpus/inferred-map-patterns.mjs new file mode 100644 index 000000000..7bd88a845 --- /dev/null +++ b/tests/corpus/inferred-map-patterns.mjs @@ -0,0 +1,6 @@ +function entries(map) { + for (const [, value] of map) console.log(value); + for (const entry of map) console.log(entry[0], entry[1]); + for (const [key, value = 4] of map) console.log(key, value); +} +entries(new Map([["a", 1], ["b", 2]])); diff --git a/tests/corpus/inferred-method-override.js b/tests/corpus/inferred-method-override.js new file mode 100644 index 000000000..6444d0d7a --- /dev/null +++ b/tests/corpus/inferred-method-override.js @@ -0,0 +1,6 @@ +class Base { copy(value = 1) { return value; } } +class Child extends Base { copy(value) { console.log("child", value); return value; } } +const child = new Child(); +console.log(child.copy(2), new Base().copy()); +function visit(value) { return value.copy(3); } +console.log(visit(child)); diff --git a/tests/corpus/inferred-partial-destructuring.mjs b/tests/corpus/inferred-partial-destructuring.mjs new file mode 100644 index 000000000..1d0c0e1e4 --- /dev/null +++ b/tests/corpus/inferred-partial-destructuring.mjs @@ -0,0 +1,6 @@ +function invoke(callback, value) { return callback(value); } +const first = ([head]) => head; +console.log(invoke(first, [1, 2, 3]), invoke(first, [])); +console.log(invoke(([head = 4]) => head, [])); +console.log(invoke(([, second]) => second, [1, 2, 3])); +console.log(invoke(([head, ...tail]) => head + ":" + tail.join(","), [1, 2, 3])); diff --git a/tests/corpus/inferred-primitive-properties.mjs b/tests/corpus/inferred-primitive-properties.mjs new file mode 100644 index 000000000..b1b40da4f --- /dev/null +++ b/tests/corpus/inferred-primitive-properties.mjs @@ -0,0 +1,5 @@ +function isObject(value) { return typeof value === "object" && value !== null; } +function minimum(value) { return isObject(value) ? value.min : value; } +console.log(minimum(3), minimum({ min: 7 })); +function read(value) { return value.missing; } +console.log(read(1), read(false), read(2n), read("text")); diff --git a/tests/corpus/inferred-record-return.mjs b/tests/corpus/inferred-record-return.mjs new file mode 100644 index 000000000..cb9250cc4 --- /dev/null +++ b/tests/corpus/inferred-record-return.mjs @@ -0,0 +1,8 @@ +const empty = { _tag: "Empty" }; +function combine(first, second) { return { _tag: "Then", first, second }; } +function build(values) { + let result = empty; + for (const value of values) result = combine(result, value); + return result; +} +console.log(JSON.stringify(build([1, 2]))); diff --git a/tests/corpus/inferred-recursive-record.mjs b/tests/corpus/inferred-recursive-record.mjs new file mode 100644 index 000000000..8d7208583 --- /dev/null +++ b/tests/corpus/inferred-recursive-record.mjs @@ -0,0 +1,21 @@ +class Queue { + head = { value: 0, next: null }; + tail = this.head; + append(value) { + this.tail.next = { value, next: null }; + this.tail = this.tail.next; + } + values() { + const result = []; + let current = this.head.next; + while (current !== null) { + result.push(current.value); + current = current.next; + } + return result; + } +} +const queue = new Queue(); +queue.append(20); +queue.append(22); +console.log(queue.values().join(",")); diff --git a/tests/corpus/inferred-undefined-field.mjs b/tests/corpus/inferred-undefined-field.mjs new file mode 100644 index 000000000..ed56bb527 --- /dev/null +++ b/tests/corpus/inferred-undefined-field.mjs @@ -0,0 +1,11 @@ +class Executor { + current = undefined; + set(value) { this.current = value; } + run() { + const operation = this.current; + console.log(operation._tag, operation.value); + } +} +const executor = new Executor(); +executor.set({ _tag: "Ready", value: 42 }); +executor.run(); diff --git a/tests/corpus/json-runtime-options.mjs b/tests/corpus/json-runtime-options.mjs new file mode 100644 index 000000000..cdac3b41d --- /dev/null +++ b/tests/corpus/json-runtime-options.mjs @@ -0,0 +1,17 @@ +function roundtrip(text, options) { + const value = JSON.parse(text, options?.reviver); + return JSON.stringify(value, options?.replacer, options?.space); +} +console.log(roundtrip('{"n":42}')); +console.log(roundtrip('{"n":42}', { reviver: null, replacer: null, space: 2 })); +console.log(roundtrip('{"n":42}', { + reviver(key, value) { return key === "n" ? value + 1 : value; }, + replacer(key, value) { return key === "n" ? value * 2 : value; }, + space: "--", +})); +let calls = 0; +try { + roundtrip("{", { reviver() { calls++; } }); +} catch (error) { console.log(error.name, calls); } +const gap = process.argv[2]; +console.log(JSON.stringify({ n: 1 }, (key, value) => value, gap)); diff --git a/tests/corpus/nullable-object-extension.mjs b/tests/corpus/nullable-object-extension.mjs new file mode 100644 index 000000000..063f78278 --- /dev/null +++ b/tests/corpus/nullable-object-extension.mjs @@ -0,0 +1,16 @@ +function base(enabled) { + if (!enabled) return null; + return { value: 7 }; +} +function extended(enabled) { + const result = base(enabled); + if (result) { + result.extra = { value: 42 }; + const alias = result; + alias.label = "ready"; + } + return result; +} +console.log(extended(false) === null); +const result = extended(true); +console.log(result.value, result.extra.value, result.label); diff --git a/tests/corpus/object-prototype-identity.mjs b/tests/corpus/object-prototype-identity.mjs new file mode 100644 index 000000000..8664042cf --- /dev/null +++ b/tests/corpus/object-prototype-identity.mjs @@ -0,0 +1,12 @@ +function prototype(value) { + return Object.getPrototypeOf(value); +} +const plain = { value: 42 }; +const bare = Object.create(null); +const child = Object.create(plain); +console.log(prototype(plain) === Object.prototype); +console.log(prototype({ other: true }) === prototype(plain)); +console.log(prototype(Object.prototype) === null, prototype(bare) === null); +console.log(prototype([]) === Array.prototype); +console.log(prototype(Array.prototype) === Object.prototype); +console.log(prototype(child) === plain, child.value); diff --git a/tests/corpus/optional-callback-arity.ts b/tests/corpus/optional-callback-arity.ts new file mode 100644 index 000000000..6acdd5bac --- /dev/null +++ b/tests/corpus/optional-callback-arity.ts @@ -0,0 +1,8 @@ +type Parser = ((value: unknown, options: unknown) => unknown) | undefined; +function choose(enabled: boolean): Parser { + if (enabled) return (value: unknown) => value; + return undefined; +} +const parse = choose(true); +if (parse) console.log(parse("value", { strict: true })); +console.log(choose(false) === undefined); diff --git a/tests/corpus/promise-checked-rejections.ts b/tests/corpus/promise-checked-rejections.ts new file mode 100644 index 000000000..98e55d07e --- /dev/null +++ b/tests/corpus/promise-checked-rejections.ts @@ -0,0 +1,3 @@ +Promise.reject("bad").catch((reason: unknown) => console.log(reason)); +Promise.reject(42).catch((reason: unknown) => console.log(reason)); +Promise.reject(null).catch((reason: unknown) => console.log(reason)); diff --git a/tests/corpus/promise-resolve-void.ts b/tests/corpus/promise-resolve-void.ts new file mode 100644 index 000000000..cc7de9201 --- /dev/null +++ b/tests/corpus/promise-resolve-void.ts @@ -0,0 +1,5 @@ +let calls = 0; +function step(): void { calls++; console.log("argument", calls); } +Promise.resolve(step()).then((value) => console.log("fulfilled", value, calls)); +Promise.resolve(void 0).then((value) => console.log("undefined", value)); +console.log("sync", calls); diff --git a/tests/corpus/reflect-own-keys.mjs b/tests/corpus/reflect-own-keys.mjs new file mode 100644 index 000000000..ad9871d3b --- /dev/null +++ b/tests/corpus/reflect-own-keys.mjs @@ -0,0 +1,18 @@ +const first = Symbol("first"), second = Symbol("second"); +const value = { b: 1, 10: "ten", 2: "two", [first]: 3, a: 4, [second]: 5 }; +Object.defineProperty(value, "hidden", { value: 6 }); +const keys = Reflect.ownKeys; +console.log(Reflect.ownKeys(value).map((key) => String(key)).join(",")); +console.log(keys(value).map((key) => String(key)).join(","), keys.length, keys.name); +console.log(keys([1, 2]).join(",")); +for (const key of Reflect.ownKeys(value)) { + console.log(String(key), value[key], Object.hasOwn(value, key)); + console.log(Object.prototype.hasOwnProperty.call(value, key), Object.prototype.propertyIsEnumerable.call(value, key)); + if (key !== "hidden") { + value[key] = "updated"; + console.log(value[key]); + } +} +for (const primitive of [undefined, null, "text", 3, true, 4n, first]) { + try { keys(primitive); } catch (error) { console.log(error.name, error.message); } +} diff --git a/tests/corpus/reflect-record-union-keys.mjs b/tests/corpus/reflect-record-union-keys.mjs new file mode 100644 index 000000000..ebe22cad6 --- /dev/null +++ b/tests/corpus/reflect-record-union-keys.mjs @@ -0,0 +1,5 @@ +/** @param {{ count: number, label: string }} record */ +function read(record) { + for (const key of Reflect.ownKeys(record)) console.log(String(key), record[key]); +} +read({ count: 7, label: "record" }); diff --git a/tests/corpus/reflect-typed-record-keys.ts b/tests/corpus/reflect-typed-record-keys.ts new file mode 100644 index 000000000..681c432a5 --- /dev/null +++ b/tests/corpus/reflect-typed-record-keys.ts @@ -0,0 +1,5 @@ +const key = Symbol("key"); +const value: { label: string; [key]: number } = { label: "record", [key]: 42 }; +for (const ownKey of Reflect.ownKeys(value)) { + console.log(String(ownKey), value[ownKey as keyof typeof value]); +} diff --git a/tests/corpus/string-split-checked-separator.mjs b/tests/corpus/string-split-checked-separator.mjs new file mode 100644 index 000000000..eba033eb1 --- /dev/null +++ b/tests/corpus/string-split-checked-separator.mjs @@ -0,0 +1,4 @@ +function split(value, separator) { + console.log(JSON.stringify(value.split(separator))); +} +for (const separator of [",", 2, true, 3n, null, undefined]) split("a,2true3nullz", separator); diff --git a/tests/corpus/typed-collection-views/main.ts b/tests/corpus/typed-collection-views/main.ts new file mode 100644 index 000000000..4bf587ca2 --- /dev/null +++ b/tests/corpus/typed-collection-views/main.ts @@ -0,0 +1,15 @@ +import { changeMap, changeSet, identity } from "./use.mjs"; + +const map = new Map([["first", 1]]); +changeMap(map); +console.log(map.size, map.get("second"), identity(map) === map); +const restored = identity(map) as Map; +restored.set("third", 3); +console.log(map.get("third")); + +const set = new Set(["first"]); +changeSet(set); +console.log(set.size, set.has("second"), identity(set) === set); +const restoredSet = identity(set) as Set; +restoredSet.add("third"); +console.log(set.has("third")); diff --git a/tests/corpus/typed-collection-views/use.mjs b/tests/corpus/typed-collection-views/use.mjs new file mode 100644 index 000000000..548395324 --- /dev/null +++ b/tests/corpus/typed-collection-views/use.mjs @@ -0,0 +1,20 @@ +export function identity(value) { return value; } + +export function changeMap(map) { + const iterator = map.entries(); + console.log(iterator.next().value.join(":")); + map.set("second", 2); + console.log(iterator.next().value.join(":")); + console.log(map.has(1), map.get(1), map.delete(1)); + map.forEach((value, key, same) => console.log(key, value, same === map)); + console.log(map.delete("first"), iterator.next().done); +} + +export function changeSet(set) { + const iterator = set.values(); + console.log(iterator.next().value); + set.add("second"); + console.log(iterator.next().value, set.has(1), set.delete(1)); + set.forEach((value, key, same) => console.log(value, key, same === set)); + console.log(set.delete("first"), iterator.next().done); +} diff --git a/tests/corpus/weak-native-class-identity.ts b/tests/corpus/weak-native-class-identity.ts new file mode 100644 index 000000000..15f2cba15 --- /dev/null +++ b/tests/corpus/weak-native-class-identity.ts @@ -0,0 +1,9 @@ +class Key { value = 1; } +const first = new Key(); +const second = new Key(); +const map = new WeakMap(); +const set = new WeakSet(); +const value: unknown = first; +console.log(map.set(first, 3) === map, map.get(value as object), map.has(second)); +console.log(set.add(first) === set, set.has(value as object), set.has(second)); +console.log(map.delete(first), map.has(first), set.delete(first), set.has(first)); diff --git a/tests/fixtures/effect/async-abort.ts b/tests/fixtures/effect/async-abort.ts new file mode 100644 index 000000000..40286843b --- /dev/null +++ b/tests/fixtures/effect/async-abort.ts @@ -0,0 +1,5 @@ +import { Effect } from "effect"; +const c = new AbortController(); +const result = Effect.runPromiseExit(Effect.never, { signal: c.signal }); +c.abort(); +result.then((e) => console.log(e._tag)); diff --git a/tests/fixtures/effect/async-delay.ts b/tests/fixtures/effect/async-delay.ts new file mode 100644 index 000000000..fe828a47b --- /dev/null +++ b/tests/fixtures/effect/async-delay.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.delay(Effect.succeed(42), "1 millis")).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/async-promise-inferred.ts b/tests/fixtures/effect/async-promise-inferred.ts new file mode 100644 index 000000000..5781ab3f4 --- /dev/null +++ b/tests/fixtures/effect/async-promise-inferred.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n = yield* Effect.promise(() => Promise.resolve(21)); return n * 2; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/async-promise-typed.ts b/tests/fixtures/effect/async-promise-typed.ts new file mode 100644 index 000000000..efb9314d2 --- /dev/null +++ b/tests/fixtures/effect/async-promise-typed.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n: number = yield* Effect.promise(() => Promise.resolve(21)); return n * 2; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/async-resume-sync.ts b/tests/fixtures/effect/async-resume-sync.ts new file mode 100644 index 000000000..f05a69965 --- /dev/null +++ b/tests/fixtures/effect/async-resume-sync.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.async((resume) => resume(Effect.succeed(42)))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/async-resume-timer.ts b/tests/fixtures/effect/async-resume-timer.ts new file mode 100644 index 000000000..d7c48e914 --- /dev/null +++ b/tests/fixtures/effect/async-resume-timer.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.async((resume) => { const t = setTimeout(() => resume(Effect.succeed(42)), 1); return Effect.sync(() => clearTimeout(t)); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/async-sleep.ts b/tests/fixtures/effect/async-sleep.ts new file mode 100644 index 000000000..b0371029c --- /dev/null +++ b/tests/fixtures/effect/async-sleep.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.sleep("1 millis")).then(() => console.log("slept")); diff --git a/tests/fixtures/effect/async-timeout.ts b/tests/fixtures/effect/async-timeout.ts new file mode 100644 index 000000000..c4c90f751 --- /dev/null +++ b/tests/fixtures/effect/async-timeout.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.catchTag(Effect.timeout(Effect.never, "1 millis"), "TimeoutException", () => Effect.succeed("timed out")); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/async-try-promise.ts b/tests/fixtures/effect/async-try-promise.ts new file mode 100644 index 000000000..429078b02 --- /dev/null +++ b/tests/fixtures/effect/async-try-promise.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.tryPromise({ try: () => Promise.reject("bad"), catch: (e) => "caught:" + String(e) }); +Effect.runPromise(Effect.catchAll(p, (s: string) => Effect.succeed(s))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/async-yield.ts b/tests/fixtures/effect/async-yield.ts new file mode 100644 index 000000000..bcfd31490 --- /dev/null +++ b/tests/fixtures/effect/async-yield.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.gen(function* () { yield* Effect.yieldNow(); return 42; })).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/cache-cached.ts b/tests/fixtures/effect/cache-cached.ts new file mode 100644 index 000000000..1c50e4154 --- /dev/null +++ b/tests/fixtures/effect/cache-cached.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +let calls = 0; +const p = Effect.gen(function* () { const cached = yield* Effect.cached(Effect.sync(() => ++calls)); console.log(yield* cached, yield* cached, calls); }); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/cache-function.ts b/tests/fixtures/effect/cache-function.ts new file mode 100644 index 000000000..b9f35da98 --- /dev/null +++ b/tests/fixtures/effect/cache-function.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +let calls = 0; +const p = Effect.gen(function* () { const f = yield* Effect.cachedFunction((n: number) => Effect.sync(() => { calls++; return n * 2; })); console.log(yield* f(21), yield* f(21), calls); }); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/cache-make.ts b/tests/fixtures/effect/cache-make.ts new file mode 100644 index 000000000..07b54d4a1 --- /dev/null +++ b/tests/fixtures/effect/cache-make.ts @@ -0,0 +1,4 @@ +import { Effect, Cache } from "effect"; +let calls = 0; +const p = Effect.gen(function* () { const c = yield* Cache.make({ capacity: 10, timeToLive: "1 minute", lookup: (n: number) => Effect.sync(() => { calls++; return n * 2; }) }); console.log(yield* c.get(21), yield* c.get(21), calls); }); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/clock.ts b/tests/fixtures/effect/clock.ts new file mode 100644 index 000000000..e09f9039e --- /dev/null +++ b/tests/fixtures/effect/clock.ts @@ -0,0 +1,2 @@ +import { Effect, Clock } from "effect"; +Effect.runPromise(Clock.currentTimeMillis).then((n: number) => console.log(n > 0)); diff --git a/tests/fixtures/effect/concurrency-all-async.ts b/tests/fixtures/effect/concurrency-all-async.ts new file mode 100644 index 000000000..68fdb2e59 --- /dev/null +++ b/tests/fixtures/effect/concurrency-all-async.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const task = (n: number) => Effect.gen(function* () { yield* Effect.sleep("1 millis"); return n; }); +Effect.runPromise(Effect.all([task(1), task(2)], { concurrency: 2 })).then((a: number[]) => console.log(a.join(","))); diff --git a/tests/fixtures/effect/concurrency-all-sync.ts b/tests/fixtures/effect/concurrency-all-sync.ts new file mode 100644 index 000000000..b5058bf53 --- /dev/null +++ b/tests/fixtures/effect/concurrency-all-sync.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.all([Effect.succeed(1), Effect.succeed(2)], { concurrency: 2 })).then((a: number[]) => console.log(a.join(","))); diff --git a/tests/fixtures/effect/concurrency-exit-json.ts b/tests/fixtures/effect/concurrency-exit-json.ts new file mode 100644 index 000000000..17b01c252 --- /dev/null +++ b/tests/fixtures/effect/concurrency-exit-json.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromiseExit(Effect.all([Effect.succeed(1), Effect.succeed(2)], { concurrency: 2 })).then((e: object) => console.log(JSON.stringify(e))); diff --git a/tests/fixtures/effect/concurrency-for-each.ts b/tests/fixtures/effect/concurrency-for-each.ts new file mode 100644 index 000000000..eac707bc1 --- /dev/null +++ b/tests/fixtures/effect/concurrency-for-each.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.forEach([1, 2, 3], (n: number) => Effect.succeed(n * 2), { concurrency: "unbounded" })).then((a: number[]) => console.log(a.join(","))); diff --git a/tests/fixtures/effect/concurrency-race.ts b/tests/fixtures/effect/concurrency-race.ts new file mode 100644 index 000000000..6329463e1 --- /dev/null +++ b/tests/fixtures/effect/concurrency-race.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.race(Effect.succeed(42), Effect.never)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/concurrency-validate.ts b/tests/fixtures/effect/concurrency-validate.ts new file mode 100644 index 000000000..d34c0e0a2 --- /dev/null +++ b/tests/fixtures/effect/concurrency-validate.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.validateAll([1, 2, 3], (n: number) => n === 2 ? Effect.fail("bad:2") : Effect.succeed(n)); +Effect.runPromise(Effect.match(p, { onFailure: (es) => es.join(","), onSuccess: (ns) => ns.join(",") })).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/config-default.ts b/tests/fixtures/effect/config-default.ts new file mode 100644 index 000000000..2ab6e9bd3 --- /dev/null +++ b/tests/fixtures/effect/config-default.ts @@ -0,0 +1,3 @@ +import { Effect, Config, ConfigProvider } from "effect"; +const p = Config.all({ port: Config.withDefault(Config.integer("PORT"), 3000), host: Config.string("HOST") }); +Effect.runPromise(Effect.withConfigProvider(p, ConfigProvider.fromMap(new Map([["HOST", "localhost"]])))).then((v) => console.log(v.host, v.port)); diff --git a/tests/fixtures/effect/config-provider.ts b/tests/fixtures/effect/config-provider.ts new file mode 100644 index 000000000..3da2d4c4f --- /dev/null +++ b/tests/fixtures/effect/config-provider.ts @@ -0,0 +1,3 @@ +import { Effect, Config, ConfigProvider } from "effect"; +const p = Effect.withConfigProvider(Config.integer("PORT"), ConfigProvider.fromMap(new Map([["PORT", "3000"]]))); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/config-redacted.ts b/tests/fixtures/effect/config-redacted.ts new file mode 100644 index 000000000..9a16e9b42 --- /dev/null +++ b/tests/fixtures/effect/config-redacted.ts @@ -0,0 +1,3 @@ +import { Effect, Config, ConfigProvider, Redacted } from "effect"; +const p = Effect.withConfigProvider(Config.redacted("SECRET"), ConfigProvider.fromMap(new Map([["SECRET", "fixture-only"]]))); +Effect.runPromise(p).then((v) => console.log(String(v), Redacted.value(v))); diff --git a/tests/fixtures/effect/core-all-record.ts b/tests/fixtures/effect/core-all-record.ts new file mode 100644 index 000000000..ebf97f2dd --- /dev/null +++ b/tests/fixtures/effect/core-all-record.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.all({ count: Effect.succeed(42), name: Effect.succeed("world") })).then((v) => console.log(v.count, v.name)); diff --git a/tests/fixtures/effect/core-all-tuple.ts b/tests/fixtures/effect/core-all-tuple.ts new file mode 100644 index 000000000..f1b86662f --- /dev/null +++ b/tests/fixtures/effect/core-all-tuple.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.all([Effect.succeed(1), Effect.succeed("two")] as const)).then(([a, b]) => console.log(a, b)); diff --git a/tests/fixtures/effect/core-do.ts b/tests/fixtures/effect/core-do.ts new file mode 100644 index 000000000..299c5c489 --- /dev/null +++ b/tests/fixtures/effect/core-do.ts @@ -0,0 +1,3 @@ +import { Effect, pipe } from "effect"; +const p = pipe(Effect.Do, Effect.bind("a", () => Effect.succeed(20)), Effect.bind("b", ({ a }) => Effect.succeed(a + 1)), Effect.let("total", ({ a, b }) => a + b)); +Effect.runPromise(p).then((v) => console.log(v.total)); diff --git a/tests/fixtures/effect/core-fn.ts b/tests/fixtures/effect/core-fn.ts new file mode 100644 index 000000000..11234f6e1 --- /dev/null +++ b/tests/fixtures/effect/core-fn.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const double = Effect.fn("double")(function* (n: number) { return yield* Effect.succeed(n * 2); }); +Effect.runPromise(double(21)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/core-for-each.ts b/tests/fixtures/effect/core-for-each.ts new file mode 100644 index 000000000..bd9f391e1 --- /dev/null +++ b/tests/fixtures/effect/core-for-each.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.forEach([1, 2, 3], (n: number) => Effect.succeed(n * 2))).then((a: number[]) => console.log(a.join(","))); diff --git a/tests/fixtures/effect/core-gen.ts b/tests/fixtures/effect/core-gen.ts new file mode 100644 index 000000000..810ab51f1 --- /dev/null +++ b/tests/fixtures/effect/core-gen.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n = yield* Effect.succeed(20); return yield* Effect.sync(() => n + 22); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/core-loop.ts b/tests/fixtures/effect/core-loop.ts new file mode 100644 index 000000000..fce8715b1 --- /dev/null +++ b/tests/fixtures/effect/core-loop.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.loop(0, { while: (n: number) => n < 3, step: (n: number) => n + 1, body: (n: number) => Effect.if(n === 1, { onTrue: () => Effect.succeed(10), onFalse: () => Effect.succeed(n) }) }); +Effect.runPromise(p).then((a: number[]) => console.log(a.join(","))); diff --git a/tests/fixtures/effect/core-pipe.ts b/tests/fixtures/effect/core-pipe.ts new file mode 100644 index 000000000..bcc669395 --- /dev/null +++ b/tests/fixtures/effect/core-pipe.ts @@ -0,0 +1,3 @@ +import { Effect, pipe } from "effect"; +const p = pipe(Effect.succeed(20), Effect.map((n: number) => n + 1), Effect.flatMap((n: number) => Effect.succeed(n * 2)), Effect.tap((n: number) => Effect.sync(() => console.log("tap", n))), Effect.as("done")); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/core-suspend.ts b/tests/fixtures/effect/core-suspend.ts new file mode 100644 index 000000000..1fcd01011 --- /dev/null +++ b/tests/fixtures/effect/core-suspend.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +let count = 0; +const p = Effect.suspend(() => { count++; return Effect.succeed(count); }); +Effect.runPromise(Effect.gen(function* () { console.log(yield* p, yield* p); })); diff --git a/tests/fixtures/effect/data-chunk.ts b/tests/fixtures/effect/data-chunk.ts new file mode 100644 index 000000000..c54c84ceb --- /dev/null +++ b/tests/fixtures/effect/data-chunk.ts @@ -0,0 +1,2 @@ +import { Chunk } from "effect"; +console.log(Chunk.toReadonlyArray(Chunk.map(Chunk.make(1, 2), (n: number) => n * 2)).join(",")); diff --git a/tests/fixtures/effect/data-collections.ts b/tests/fixtures/effect/data-collections.ts new file mode 100644 index 000000000..2fe3521b0 --- /dev/null +++ b/tests/fixtures/effect/data-collections.ts @@ -0,0 +1,4 @@ +import { Chunk, HashMap, HashSet, Option } from "effect"; +console.log(Chunk.toReadonlyArray(Chunk.map(Chunk.make(1, 2), (n: number) => n * 2)).join(",")); +console.log(Option.getOrElse(HashMap.get(HashMap.make(["a", 42]), "a"), () => 0)); +console.log(HashSet.has(HashSet.make("a", "b"), "b")); diff --git a/tests/fixtures/effect/data-date-time.ts b/tests/fixtures/effect/data-date-time.ts new file mode 100644 index 000000000..8a7a461fc --- /dev/null +++ b/tests/fixtures/effect/data-date-time.ts @@ -0,0 +1,2 @@ +import { DateTime } from "effect"; +console.log(DateTime.formatIso(DateTime.unsafeMake("2020-01-01T00:00:00.000Z"))); diff --git a/tests/fixtures/effect/data-duration.ts b/tests/fixtures/effect/data-duration.ts new file mode 100644 index 000000000..6af1d7be1 --- /dev/null +++ b/tests/fixtures/effect/data-duration.ts @@ -0,0 +1,2 @@ +import { Duration } from "effect"; +console.log(Duration.toMillis(Duration.decode("2 seconds"))); diff --git a/tests/fixtures/effect/data-hashmap.ts b/tests/fixtures/effect/data-hashmap.ts new file mode 100644 index 000000000..5a9599d5a --- /dev/null +++ b/tests/fixtures/effect/data-hashmap.ts @@ -0,0 +1,2 @@ +import { HashMap, Option } from "effect"; +console.log(Option.getOrElse(HashMap.get(HashMap.make(["a", 42]), "a"), () => 0)); diff --git a/tests/fixtures/effect/data-hashset.ts b/tests/fixtures/effect/data-hashset.ts new file mode 100644 index 000000000..fc4929b07 --- /dev/null +++ b/tests/fixtures/effect/data-hashset.ts @@ -0,0 +1,2 @@ +import { HashSet } from "effect"; +console.log(HashSet.has(HashSet.make("a", "b"), "b")); diff --git a/tests/fixtures/effect/data-match.ts b/tests/fixtures/effect/data-match.ts new file mode 100644 index 000000000..983572f93 --- /dev/null +++ b/tests/fixtures/effect/data-match.ts @@ -0,0 +1,6 @@ +import { Match, pipe } from "effect"; +type Result = { readonly _tag: "Ok"; readonly n: number } | { readonly _tag: "Bad"; readonly message: string }; +function render(v: Result) { + return pipe(Match.value(v), Match.tag("Ok", (r) => "ok:" + r.n), Match.tag("Bad", (r) => "bad:" + r.message), Match.exhaustive); +} +console.log(render({ _tag: "Ok", n: 42 }), render({ _tag: "Bad", message: "oops" })); diff --git a/tests/fixtures/effect/data-option-either.ts b/tests/fixtures/effect/data-option-either.ts new file mode 100644 index 000000000..0ad79628a --- /dev/null +++ b/tests/fixtures/effect/data-option-either.ts @@ -0,0 +1,3 @@ +import { Option, Either } from "effect"; +console.log(Option.getOrElse(Option.map(Option.some(21), (n: number) => n * 2), () => 0)); +console.log(Either.getOrElse(Either.map(Either.right(21), (n: number) => n * 2), () => 0)); diff --git a/tests/fixtures/effect/data-struct-equality.ts b/tests/fixtures/effect/data-struct-equality.ts new file mode 100644 index 000000000..0f867b18e --- /dev/null +++ b/tests/fixtures/effect/data-struct-equality.ts @@ -0,0 +1,2 @@ +import { Data, Equal } from "effect"; +console.log(Equal.equals(Data.struct({ n: 42 }), Data.struct({ n: 42 }))); diff --git a/tests/fixtures/effect/data-tagged-class.ts b/tests/fixtures/effect/data-tagged-class.ts new file mode 100644 index 000000000..3551786e5 --- /dev/null +++ b/tests/fixtures/effect/data-tagged-class.ts @@ -0,0 +1,4 @@ +import { Data } from "effect"; +class Person extends Data.TaggedClass("Person")<{ readonly name: string }> {} +const p = new Person({ name: "Ada" }); +console.log(p._tag, p.name); diff --git a/tests/fixtures/effect/data-yield-option-either.ts b/tests/fixtures/effect/data-yield-option-either.ts new file mode 100644 index 000000000..ca0e31b1a --- /dev/null +++ b/tests/fixtures/effect/data-yield-option-either.ts @@ -0,0 +1,3 @@ +import { Effect, Option, Either } from "effect"; +const p = Effect.gen(function* () { const a = yield* Option.some(20); const b = yield* Either.right(22); return a + b; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/effect-subpath.ts b/tests/fixtures/effect/effect-subpath.ts new file mode 100644 index 000000000..6feb491b8 --- /dev/null +++ b/tests/fixtures/effect/effect-subpath.ts @@ -0,0 +1,2 @@ +import * as Effect from "effect/Effect"; +Effect.runPromise(Effect.gen(function* () { yield* Effect.succeed("world"); return 42; })).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/either-json.ts b/tests/fixtures/effect/either-json.ts new file mode 100644 index 000000000..69162f1ae --- /dev/null +++ b/tests/fixtures/effect/either-json.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.either(Effect.fail("bad"))).then((e: object) => console.log(JSON.stringify(e))); diff --git a/tests/fixtures/effect/error-catch-all.ts b/tests/fixtures/effect/error-catch-all.ts new file mode 100644 index 000000000..67b0c48d0 --- /dev/null +++ b/tests/fixtures/effect/error-catch-all.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.catchAll(Effect.fail("nope"), (s: string) => Effect.succeed("recovered:" + s))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/error-cause.ts b/tests/fixtures/effect/error-cause.ts new file mode 100644 index 000000000..bb0c07557 --- /dev/null +++ b/tests/fixtures/effect/error-cause.ts @@ -0,0 +1,2 @@ +import { Effect, Cause, Option } from "effect"; +Effect.runPromise(Effect.exit(Effect.fail("bad"))).then((e) => { if (e._tag === "Failure") { console.log(Option.getOrElse(Cause.failureOption(e.cause), () => "missing")); console.log(Cause.pretty(e.cause)); } }); diff --git a/tests/fixtures/effect/error-defect.ts b/tests/fixtures/effect/error-defect.ts new file mode 100644 index 000000000..c7e0eb7e5 --- /dev/null +++ b/tests/fixtures/effect/error-defect.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.catchAllDefect(Effect.die("boom"), (e) => Effect.succeed(String(e)))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/error-either-structural.ts b/tests/fixtures/effect/error-either-structural.ts new file mode 100644 index 000000000..c662f5cd4 --- /dev/null +++ b/tests/fixtures/effect/error-either-structural.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.either(Effect.fail("bad"))).then((e: { readonly _tag: string }) => console.log(e._tag)); diff --git a/tests/fixtures/effect/error-either.ts b/tests/fixtures/effect/error-either.ts new file mode 100644 index 000000000..2c55be0fc --- /dev/null +++ b/tests/fixtures/effect/error-either.ts @@ -0,0 +1,2 @@ +import { Effect, Either } from "effect"; +Effect.runPromise(Effect.either(Effect.fail("bad"))).then((e) => console.log(Either.match(e, { onLeft: (s: string) => "left:" + s, onRight: () => "right" }))); diff --git a/tests/fixtures/effect/error-map.ts b/tests/fixtures/effect/error-map.ts new file mode 100644 index 000000000..eaaf42f7d --- /dev/null +++ b/tests/fixtures/effect/error-map.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +const p = Effect.mapError(Effect.fail(21), (n: number) => "bad:" + n); +Effect.runPromise(Effect.catchAll(p, (s: string) => Effect.succeed(s))).then((s: string) => console.log(s)); +Effect.runPromise(Effect.orElse(Effect.fail("bad"), () => Effect.succeed("fallback"))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/error-match.ts b/tests/fixtures/effect/error-match.ts new file mode 100644 index 000000000..ee813ebb2 --- /dev/null +++ b/tests/fixtures/effect/error-match.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.match(Effect.fail("bad"), { onFailure: (s: string) => "failed:" + s, onSuccess: () => "ok" }); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/error-option.ts b/tests/fixtures/effect/error-option.ts new file mode 100644 index 000000000..1c793da40 --- /dev/null +++ b/tests/fixtures/effect/error-option.ts @@ -0,0 +1,2 @@ +import { Effect, Option } from "effect"; +Effect.runPromise(Effect.option(Effect.fail("bad"))).then((o) => console.log(Option.isNone(o))); diff --git a/tests/fixtures/effect/error-tagged-class.ts b/tests/fixtures/effect/error-tagged-class.ts new file mode 100644 index 000000000..1d31f8dfc --- /dev/null +++ b/tests/fixtures/effect/error-tagged-class.ts @@ -0,0 +1,3 @@ +import { Data, Effect } from "effect"; +class Missing extends Data.TaggedError("Missing")<{ readonly id: number }> {} +Effect.runPromise(Effect.catchTag(Effect.fail(new Missing({ id: 7 })), "Missing", (e) => Effect.succeed(e.id))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/error-tagged-plain.ts b/tests/fixtures/effect/error-tagged-plain.ts new file mode 100644 index 000000000..e450b6d25 --- /dev/null +++ b/tests/fixtures/effect/error-tagged-plain.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.catchTag(Effect.fail({ _tag: "Missing" as const, id: 7 }), "Missing", (e) => Effect.succeed(e.id + 1)); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/error-try.ts b/tests/fixtures/effect/error-try.ts new file mode 100644 index 000000000..ace57ed67 --- /dev/null +++ b/tests/fixtures/effect/error-try.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.try({ try: () => { throw new Error("bad"); }, catch: (e) => String(e) }); +Effect.runPromise(Effect.catchAll(p, (s: string) => Effect.succeed("caught:" + s))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/exit-structural.ts b/tests/fixtures/effect/exit-structural.ts new file mode 100644 index 000000000..7aa750ba5 --- /dev/null +++ b/tests/fixtures/effect/exit-structural.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromiseExit(Effect.fail("bad")).then((e: { readonly _tag: string }) => console.log(e._tag)); diff --git a/tests/fixtures/effect/fiber-fork-join.ts b/tests/fixtures/effect/fiber-fork-join.ts new file mode 100644 index 000000000..8f504a307 --- /dev/null +++ b/tests/fixtures/effect/fiber-fork-join.ts @@ -0,0 +1,3 @@ +import { Effect, Fiber } from "effect"; +const p = Effect.gen(function* () { const fiber = yield* Effect.fork(Effect.succeed(42)); return yield* Fiber.join(fiber); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/fiber-interrupt.ts b/tests/fixtures/effect/fiber-interrupt.ts new file mode 100644 index 000000000..0a0d178f3 --- /dev/null +++ b/tests/fixtures/effect/fiber-interrupt.ts @@ -0,0 +1,8 @@ +import { Effect, Fiber } from "effect"; +const p = Effect.gen(function* () { + const f = yield* Effect.fork(Effect.onInterrupt(Effect.never, () => Effect.sync(() => console.log("interrupted")))); + yield* Effect.yieldNow(); + const e = yield* Fiber.interrupt(f); + return e._tag; +}); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/fiber-run-fork.ts b/tests/fixtures/effect/fiber-run-fork.ts new file mode 100644 index 000000000..9c5fd3427 --- /dev/null +++ b/tests/fixtures/effect/fiber-run-fork.ts @@ -0,0 +1,3 @@ +import { Effect, Fiber } from "effect"; +const fiber = Effect.runFork(Effect.succeed(42)); +Effect.runPromise(Fiber.join(fiber)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/gen-baseline.ts b/tests/fixtures/effect/gen-baseline.ts new file mode 100644 index 000000000..d6c883c76 --- /dev/null +++ b/tests/fixtures/effect/gen-baseline.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const name = yield* Effect.succeed("world"); yield* Effect.sync(() => console.log("Hello, " + name + "!")); return 42; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/gen-captured-sync.ts b/tests/fixtures/effect/gen-captured-sync.ts new file mode 100644 index 000000000..afb58f902 --- /dev/null +++ b/tests/fixtures/effect/gen-captured-sync.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n = yield* Effect.succeed(20); const result = yield* Effect.sync(() => n + 22); return result; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/gen-number-inferred.ts b/tests/fixtures/effect/gen-number-inferred.ts new file mode 100644 index 000000000..ebfffb64e --- /dev/null +++ b/tests/fixtures/effect/gen-number-inferred.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n = yield* Effect.succeed(42); console.log("yielded", n); return n; }); +Effect.runPromise(p).then((n: number) => console.log("returned", n)); diff --git a/tests/fixtures/effect/gen-number-typed.ts b/tests/fixtures/effect/gen-number-typed.ts new file mode 100644 index 000000000..63b23cd26 --- /dev/null +++ b/tests/fixtures/effect/gen-number-typed.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n: number = yield* Effect.succeed(42); console.log("yielded", n); return n; }); +Effect.runPromise(p).then((n: number) => console.log("returned", n)); diff --git a/tests/fixtures/effect/gen-separate-sync.ts b/tests/fixtures/effect/gen-separate-sync.ts new file mode 100644 index 000000000..b53bd344f --- /dev/null +++ b/tests/fixtures/effect/gen-separate-sync.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const n = yield* Effect.sync(() => 42); return n; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/gen-string-result.ts b/tests/fixtures/effect/gen-string-result.ts new file mode 100644 index 000000000..b75bda867 --- /dev/null +++ b/tests/fixtures/effect/gen-string-result.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const name = yield* Effect.succeed("world"); return name; }); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/gen-tail-succeed.ts b/tests/fixtures/effect/gen-tail-succeed.ts new file mode 100644 index 000000000..8644c1d02 --- /dev/null +++ b/tests/fixtures/effect/gen-tail-succeed.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.gen(function* () { return yield* Effect.succeed(42); })).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/layer-compose.ts b/tests/fixtures/effect/layer-compose.ts new file mode 100644 index 000000000..ce0545597 --- /dev/null +++ b/tests/fixtures/effect/layer-compose.ts @@ -0,0 +1,7 @@ +import { Context, Effect, Layer } from "effect"; +const A = Context.GenericTag("A"); +const B = Context.GenericTag("B"); +const C = Context.GenericTag("C"); +const ab = Layer.merge(Layer.succeed(A, 42), Layer.succeed(B, "world")); +const c = Layer.effect(C, Effect.gen(function* () { const a = yield* A; const b = yield* B; return b + ":" + a; })); +Effect.runPromise(Effect.provide(C, Layer.provide(c, ab))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/layer-effect.ts b/tests/fixtures/effect/layer-effect.ts new file mode 100644 index 000000000..aaf9a738f --- /dev/null +++ b/tests/fixtures/effect/layer-effect.ts @@ -0,0 +1,3 @@ +import { Context, Effect, Layer } from "effect"; +const Count = Context.GenericTag("Count"); +Effect.runPromise(Effect.provide(Count, Layer.effect(Count, Effect.succeed(42)))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/layer-empty.ts b/tests/fixtures/effect/layer-empty.ts new file mode 100644 index 000000000..3eb48d971 --- /dev/null +++ b/tests/fixtures/effect/layer-empty.ts @@ -0,0 +1,2 @@ +import { Effect, Layer } from "effect"; +Effect.runPromise(Effect.provide(Effect.succeed(42), Layer.empty)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/layer-scoped.ts b/tests/fixtures/effect/layer-scoped.ts new file mode 100644 index 000000000..0aaa3f38e --- /dev/null +++ b/tests/fixtures/effect/layer-scoped.ts @@ -0,0 +1,5 @@ +import { Context, Effect, Layer } from "effect"; +const Count = Context.GenericTag("Count"); +let closed = false; +const live = Layer.scoped(Count, Effect.acquireRelease(Effect.succeed(42), () => Effect.sync(() => { closed = true; }))); +Effect.runPromise(Effect.provide(Count, live)).then((n: number) => console.log(n, closed)); diff --git a/tests/fixtures/effect/layer-succeed.ts b/tests/fixtures/effect/layer-succeed.ts new file mode 100644 index 000000000..86964ed76 --- /dev/null +++ b/tests/fixtures/effect/layer-succeed.ts @@ -0,0 +1,3 @@ +import { Context, Effect, Layer } from "effect"; +const Count = Context.GenericTag("Count"); +Effect.runPromise(Effect.provide(Count, Layer.succeed(Count, 42))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/log-custom.ts b/tests/fixtures/effect/log-custom.ts new file mode 100644 index 000000000..6a13a5ca3 --- /dev/null +++ b/tests/fixtures/effect/log-custom.ts @@ -0,0 +1,4 @@ +import { Effect, Logger, HashMap, Option } from "effect"; +const logger = Logger.make(({ logLevel, message, annotations }) => console.log(logLevel.label, String(message), Option.getOrElse(HashMap.get(annotations, "request"), () => ""))); +const p = Effect.annotateLogs(Effect.log("hello"), "request", "abc"); +Effect.runPromise(Effect.provide(p, Logger.replace(Logger.defaultLogger, logger))).then(() => console.log("done")); diff --git a/tests/fixtures/effect/metric-counter.ts b/tests/fixtures/effect/metric-counter.ts new file mode 100644 index 000000000..8618c54c8 --- /dev/null +++ b/tests/fixtures/effect/metric-counter.ts @@ -0,0 +1,3 @@ +import { Effect, Metric } from "effect"; +const counter = Metric.counter("requests"); +Effect.runPromise(Effect.gen(function* () { yield* Metric.increment(counter); const v = yield* Metric.value(counter); console.log(v.count); })); diff --git a/tests/fixtures/effect/package.json b/tests/fixtures/effect/package.json new file mode 100644 index 000000000..f84528506 --- /dev/null +++ b/tests/fixtures/effect/package.json @@ -0,0 +1 @@ +{"private":true,"type":"module"} diff --git a/tests/fixtures/effect/pipe-basic.ts b/tests/fixtures/effect/pipe-basic.ts new file mode 100644 index 000000000..c1bfb5107 --- /dev/null +++ b/tests/fixtures/effect/pipe-basic.ts @@ -0,0 +1,3 @@ +import { Effect, pipe } from "effect"; +const p = pipe(Effect.succeed(20), Effect.map((n: number) => n + 1), Effect.flatMap((n: number) => Effect.succeed(n * 2))); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/pool.ts b/tests/fixtures/effect/pool.ts new file mode 100644 index 000000000..f518c1667 --- /dev/null +++ b/tests/fixtures/effect/pool.ts @@ -0,0 +1,3 @@ +import { Effect, Pool } from "effect"; +const p = Effect.scoped(Effect.gen(function* () { const pool = yield* Pool.make({ acquire: Effect.succeed(42), size: 1 }); return yield* Pool.get(pool); })); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/pubsub.ts b/tests/fixtures/effect/pubsub.ts new file mode 100644 index 000000000..295d3a135 --- /dev/null +++ b/tests/fixtures/effect/pubsub.ts @@ -0,0 +1,3 @@ +import { Effect, PubSub, Queue } from "effect"; +const p = Effect.scoped(Effect.gen(function* () { const hub = yield* PubSub.unbounded(); const a = yield* PubSub.subscribe(hub); const b = yield* PubSub.subscribe(hub); yield* PubSub.publish(hub, 42); console.log(yield* Queue.take(a), yield* Queue.take(b)); yield* PubSub.shutdown(hub); })); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/queue-backpressure.ts b/tests/fixtures/effect/queue-backpressure.ts new file mode 100644 index 000000000..4f758bed5 --- /dev/null +++ b/tests/fixtures/effect/queue-backpressure.ts @@ -0,0 +1,3 @@ +import { Effect, Queue, Fiber } from "effect"; +const p = Effect.gen(function* () { const q = yield* Queue.bounded(1); yield* Queue.offer(q, 1); const f = yield* Effect.fork(Queue.offer(q, 2)); const a = yield* Queue.take(q); yield* Fiber.join(f); const b = yield* Queue.take(q); yield* Queue.shutdown(q); console.log(a, b); }); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/queue-unbounded.ts b/tests/fixtures/effect/queue-unbounded.ts new file mode 100644 index 000000000..4b3a7aeee --- /dev/null +++ b/tests/fixtures/effect/queue-unbounded.ts @@ -0,0 +1,3 @@ +import { Effect, Queue } from "effect"; +const p = Effect.gen(function* () { const q = yield* Queue.unbounded(); yield* Queue.offer(q, 42); const n = yield* Queue.take(q); yield* Queue.shutdown(q); return n; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/random.ts b/tests/fixtures/effect/random.ts new file mode 100644 index 000000000..cc6b711cb --- /dev/null +++ b/tests/fixtures/effect/random.ts @@ -0,0 +1,2 @@ +import { Effect, Random } from "effect"; +Effect.runPromise(Random.nextIntBetween(1, 2)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/request-batching.ts b/tests/fixtures/effect/request-batching.ts new file mode 100644 index 000000000..4b7f6efef --- /dev/null +++ b/tests/fixtures/effect/request-batching.ts @@ -0,0 +1,5 @@ +import { Effect, Request, RequestResolver } from "effect"; +interface Double extends Request.Request { readonly _tag: "Double"; readonly value: number; } +const Double = Request.tagged("Double"); +const resolver = RequestResolver.fromFunction((request: Double) => request.value * 2); +Effect.runPromise(Effect.request(Double({ value: 21 }), resolver)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/resource-acquire-use-release.ts b/tests/fixtures/effect/resource-acquire-use-release.ts new file mode 100644 index 000000000..8f518d938 --- /dev/null +++ b/tests/fixtures/effect/resource-acquire-use-release.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.acquireUseRelease(Effect.sync(() => { console.log("acquire"); return 21; }), (n: number) => Effect.succeed(n * 2), () => Effect.sync(() => console.log("release"))); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/resource-ensuring.ts b/tests/fixtures/effect/resource-ensuring.ts new file mode 100644 index 000000000..718158fce --- /dev/null +++ b/tests/fixtures/effect/resource-ensuring.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.ensuring(Effect.succeed(42), Effect.sync(() => console.log("cleanup"))); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/resource-finalizer-failure.ts b/tests/fixtures/effect/resource-finalizer-failure.ts new file mode 100644 index 000000000..f355e8f50 --- /dev/null +++ b/tests/fixtures/effect/resource-finalizer-failure.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.scoped(Effect.gen(function* () { yield* Effect.addFinalizer(() => Effect.sync(() => console.log("finalizer"))); return yield* Effect.fail("bad"); })); +Effect.runPromise(Effect.exit(p)).then((e) => console.log(e._tag)); diff --git a/tests/fixtures/effect/resource-finalizer.ts b/tests/fixtures/effect/resource-finalizer.ts new file mode 100644 index 000000000..8aeed5eb4 --- /dev/null +++ b/tests/fixtures/effect/resource-finalizer.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.scoped(Effect.gen(function* () { yield* Effect.addFinalizer(() => Effect.sync(() => console.log("finalizer"))); return 42; })); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/resource-on-error.ts b/tests/fixtures/effect/resource-on-error.ts new file mode 100644 index 000000000..c6e8874c6 --- /dev/null +++ b/tests/fixtures/effect/resource-on-error.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.onError(Effect.fail("bad"), () => Effect.sync(() => console.log("cleanup"))); +Effect.runPromise(Effect.catchAll(p, (s: string) => Effect.succeed(s))).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/resource-scoped.ts b/tests/fixtures/effect/resource-scoped.ts new file mode 100644 index 000000000..7e71d539d --- /dev/null +++ b/tests/fixtures/effect/resource-scoped.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +let released = false; +const r = Effect.acquireRelease(Effect.succeed("resource"), () => Effect.sync(() => { released = true; })); +Effect.runPromise(Effect.scoped(r)).then((s: string) => console.log(s, released)); diff --git a/tests/fixtures/effect/runner-promise-exit.ts b/tests/fixtures/effect/runner-promise-exit.ts new file mode 100644 index 000000000..6e4d20455 --- /dev/null +++ b/tests/fixtures/effect/runner-promise-exit.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromiseExit(Effect.fail("nope")).then((exit) => console.log(exit._tag)); diff --git a/tests/fixtures/effect/runner-sync-exit.ts b/tests/fixtures/effect/runner-sync-exit.ts new file mode 100644 index 000000000..f2c7c327d --- /dev/null +++ b/tests/fixtures/effect/runner-sync-exit.ts @@ -0,0 +1,3 @@ +import { Effect, Exit } from "effect"; +console.log(Exit.isSuccess(Effect.runSyncExit(Effect.succeed(42)))); +console.log(Exit.isFailure(Effect.runSyncExit(Effect.fail("nope")))); diff --git a/tests/fixtures/effect/runner-sync.ts b/tests/fixtures/effect/runner-sync.ts new file mode 100644 index 000000000..447953969 --- /dev/null +++ b/tests/fixtures/effect/runner-sync.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +console.log(Effect.runSync(Effect.succeed(42))); diff --git a/tests/fixtures/effect/runtime-managed.ts b/tests/fixtures/effect/runtime-managed.ts new file mode 100644 index 000000000..11fc095eb --- /dev/null +++ b/tests/fixtures/effect/runtime-managed.ts @@ -0,0 +1,4 @@ +import { Context, Layer, ManagedRuntime } from "effect"; +const Count = Context.GenericTag("Count"); +const runtime = ManagedRuntime.make(Layer.succeed(Count, 42)); +runtime.runPromise(Count).then((n: number) => { console.log(n); return runtime.dispose(); }).then(() => console.log("disposed")); diff --git a/tests/fixtures/effect/schedule-backoff.ts b/tests/fixtures/effect/schedule-backoff.ts new file mode 100644 index 000000000..7b3a89904 --- /dev/null +++ b/tests/fixtures/effect/schedule-backoff.ts @@ -0,0 +1,5 @@ +import { Effect, Schedule } from "effect"; +let count = 0; +const p = Effect.suspend(() => ++count < 2 ? Effect.fail("again") : Effect.succeed(count)); +const policy = Schedule.intersect(Schedule.exponential("1 millis"), Schedule.recurs(1)); +Effect.runPromise(Effect.retry(p, policy)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/schedule-repeat.ts b/tests/fixtures/effect/schedule-repeat.ts new file mode 100644 index 000000000..d66d42545 --- /dev/null +++ b/tests/fixtures/effect/schedule-repeat.ts @@ -0,0 +1,3 @@ +import { Effect, Schedule } from "effect"; +let count = 0; +Effect.runPromise(Effect.repeat(Effect.sync(() => ++count), Schedule.recurs(2))).then((n: number) => console.log(n, count)); diff --git a/tests/fixtures/effect/schedule-retry-count.ts b/tests/fixtures/effect/schedule-retry-count.ts new file mode 100644 index 000000000..1ea4d06ec --- /dev/null +++ b/tests/fixtures/effect/schedule-retry-count.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +let count = 0; +const p = Effect.suspend(() => ++count < 3 ? Effect.fail("again") : Effect.succeed(count)); +Effect.runPromise(Effect.retry(p, { times: 2 })).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/schedule-retry-policy.ts b/tests/fixtures/effect/schedule-retry-policy.ts new file mode 100644 index 000000000..b12af5da2 --- /dev/null +++ b/tests/fixtures/effect/schedule-retry-policy.ts @@ -0,0 +1,4 @@ +import { Effect, Schedule } from "effect"; +let count = 0; +const p = Effect.suspend(() => ++count < 3 ? Effect.fail("again") : Effect.succeed(count)); +Effect.runPromise(Effect.retry(p, Schedule.recurs(2))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/schema-class.ts b/tests/fixtures/effect/schema-class.ts new file mode 100644 index 000000000..70eb09462 --- /dev/null +++ b/tests/fixtures/effect/schema-class.ts @@ -0,0 +1,3 @@ +import { Schema } from "effect"; +class Person extends Schema.Class("Person")({ name: Schema.String }) {} +console.log(new Person({ name: "Ada" }).name); diff --git a/tests/fixtures/effect/schema-decode.ts b/tests/fixtures/effect/schema-decode.ts new file mode 100644 index 000000000..a93cfb53f --- /dev/null +++ b/tests/fixtures/effect/schema-decode.ts @@ -0,0 +1,4 @@ +import { Schema } from "effect"; +const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }); +const p = Schema.decodeUnknownSync(Person)({ name: "Ada", age: 42 }); +console.log(p.name, p.age); diff --git a/tests/fixtures/effect/schema-effect.ts b/tests/fixtures/effect/schema-effect.ts new file mode 100644 index 000000000..90d383299 --- /dev/null +++ b/tests/fixtures/effect/schema-effect.ts @@ -0,0 +1,3 @@ +import { Schema, Effect } from "effect"; +const p = Schema.decodeUnknown(Schema.NumberFromString)("42"); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/schema-invalid.ts b/tests/fixtures/effect/schema-invalid.ts new file mode 100644 index 000000000..6339e0f33 --- /dev/null +++ b/tests/fixtures/effect/schema-invalid.ts @@ -0,0 +1,3 @@ +import { Schema, Either } from "effect"; +const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }); +console.log(Either.isLeft(Schema.decodeUnknownEither(Person)({ name: "Ada", age: "bad" }))); diff --git a/tests/fixtures/effect/schema-json.ts b/tests/fixtures/effect/schema-json.ts new file mode 100644 index 000000000..f8107febf --- /dev/null +++ b/tests/fixtures/effect/schema-json.ts @@ -0,0 +1,4 @@ +import { Schema } from "effect"; +const S = Schema.parseJson(Schema.Struct({ n: Schema.Number })); +const v = Schema.decodeUnknownSync(S)('{"n":42}'); +console.log(v.n, Schema.encodeSync(S)(v)); diff --git a/tests/fixtures/effect/schema-union-refinement.ts b/tests/fixtures/effect/schema-union-refinement.ts new file mode 100644 index 000000000..bbab01d01 --- /dev/null +++ b/tests/fixtures/effect/schema-union-refinement.ts @@ -0,0 +1,5 @@ +import { Schema, Either } from "effect"; +const Positive = Schema.Number.pipe(Schema.filter((n: number) => n > 0)); +const S = Schema.Struct({ kind: Schema.Union(Schema.Literal("a"), Schema.Literal("b")), count: Positive }); +console.log(Schema.decodeUnknownSync(S)({ kind: "a", count: 2 }).kind); +console.log(Either.isLeft(Schema.decodeUnknownEither(S)({ kind: "b", count: -1 }))); diff --git a/tests/fixtures/effect/scoped-exit-json.ts b/tests/fixtures/effect/scoped-exit-json.ts new file mode 100644 index 000000000..90e0390b5 --- /dev/null +++ b/tests/fixtures/effect/scoped-exit-json.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromiseExit(Effect.scoped(Effect.succeed(42))).then((e: object) => console.log(JSON.stringify(e))); diff --git a/tests/fixtures/effect/semaphore.ts b/tests/fixtures/effect/semaphore.ts new file mode 100644 index 000000000..6762bc68b --- /dev/null +++ b/tests/fixtures/effect/semaphore.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.gen(function* () { const s = yield* Effect.makeSemaphore(1); return yield* s.withPermits(1)(Effect.succeed(42)); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/service-class.ts b/tests/fixtures/effect/service-class.ts new file mode 100644 index 000000000..a0aa033ef --- /dev/null +++ b/tests/fixtures/effect/service-class.ts @@ -0,0 +1,4 @@ +import { Context, Effect } from "effect"; +class Config extends Context.Tag("Config")() {} +const p = Effect.gen(function* () { const c = yield* Config; return c.greeting; }); +Effect.runPromise(Effect.provideService(p, Config, { greeting: "hello" })).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/service-context.ts b/tests/fixtures/effect/service-context.ts new file mode 100644 index 000000000..5be885259 --- /dev/null +++ b/tests/fixtures/effect/service-context.ts @@ -0,0 +1,4 @@ +import { Context, Effect } from "effect"; +const Count = Context.GenericTag("Count"); +const p = Effect.map(Effect.context(), (c) => Context.get(c, Count)); +Effect.runPromise(Effect.provide(p, Context.make(Count, 42))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/service-effect-service.ts b/tests/fixtures/effect/service-effect-service.ts new file mode 100644 index 000000000..8c6e3f281 --- /dev/null +++ b/tests/fixtures/effect/service-effect-service.ts @@ -0,0 +1,4 @@ +import { Effect } from "effect"; +class Counter extends Effect.Service()("Counter", { sync: () => ({ value: 42 }) }) {} +const p = Effect.gen(function* () { const c = yield* Counter; return c.value; }); +Effect.runPromise(Effect.provide(p, Counter.Default)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/service-generic.ts b/tests/fixtures/effect/service-generic.ts new file mode 100644 index 000000000..c00110172 --- /dev/null +++ b/tests/fixtures/effect/service-generic.ts @@ -0,0 +1,4 @@ +import { Context, Effect } from "effect"; +const Config = Context.GenericTag<{ readonly greeting: string }>("Config"); +const p = Effect.gen(function* () { const c = yield* Config; return c.greeting; }); +Effect.runPromise(Effect.provideService(p, Config, { greeting: "hello" })).then((s: string) => console.log(s)); diff --git a/tests/fixtures/effect/state-deferred-fork.ts b/tests/fixtures/effect/state-deferred-fork.ts new file mode 100644 index 000000000..8a638c459 --- /dev/null +++ b/tests/fixtures/effect/state-deferred-fork.ts @@ -0,0 +1,3 @@ +import { Effect, Deferred, Fiber } from "effect"; +const p = Effect.gen(function* () { const d = yield* Deferred.make(); const f = yield* Effect.fork(Deferred.await(d)); yield* Deferred.succeed(d, 42); return yield* Fiber.join(f); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/state-deferred.ts b/tests/fixtures/effect/state-deferred.ts new file mode 100644 index 000000000..2f94cdafe --- /dev/null +++ b/tests/fixtures/effect/state-deferred.ts @@ -0,0 +1,3 @@ +import { Effect, Deferred } from "effect"; +const p = Effect.gen(function* () { const d = yield* Deferred.make(); yield* Deferred.succeed(d, 42); return yield* Deferred.await(d); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/state-fiber-ref.ts b/tests/fixtures/effect/state-fiber-ref.ts new file mode 100644 index 000000000..615d2fb8d --- /dev/null +++ b/tests/fixtures/effect/state-fiber-ref.ts @@ -0,0 +1,3 @@ +import { Effect, FiberRef } from "effect"; +const p = Effect.scoped(Effect.gen(function* () { const r = yield* FiberRef.make(1); const a = yield* Effect.locally(FiberRef.get(r), r, 42); const b = yield* FiberRef.get(r); console.log(a, b); })); +Effect.runPromise(p); diff --git a/tests/fixtures/effect/state-ref-separate.ts b/tests/fixtures/effect/state-ref-separate.ts new file mode 100644 index 000000000..f4bc3c9eb --- /dev/null +++ b/tests/fixtures/effect/state-ref-separate.ts @@ -0,0 +1,3 @@ +import { Effect, Ref } from "effect"; +const p = Effect.gen(function* () { const r = yield* Ref.make(20); yield* Ref.update(r, (n: number) => n + 22); const n: number = yield* Ref.get(r); return n; }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/state-ref.ts b/tests/fixtures/effect/state-ref.ts new file mode 100644 index 000000000..6ec50f9d4 --- /dev/null +++ b/tests/fixtures/effect/state-ref.ts @@ -0,0 +1,2 @@ +import { Effect, Ref } from "effect"; +Effect.runPromise(Effect.gen(function* () { const r = yield* Ref.make(20); yield* Ref.update(r, (n: number) => n + 22); return yield* Ref.get(r); })).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/state-sync-ref.ts b/tests/fixtures/effect/state-sync-ref.ts new file mode 100644 index 000000000..f64e8b74a --- /dev/null +++ b/tests/fixtures/effect/state-sync-ref.ts @@ -0,0 +1,3 @@ +import { Effect, SynchronizedRef } from "effect"; +const p = Effect.gen(function* () { const r = yield* SynchronizedRef.make(20); yield* SynchronizedRef.updateEffect(r, (n: number) => Effect.succeed(n + 22)); return yield* SynchronizedRef.get(r); }); +Effect.runPromise(p).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/stm-ref.ts b/tests/fixtures/effect/stm-ref.ts new file mode 100644 index 000000000..dc3ff5ab2 --- /dev/null +++ b/tests/fixtures/effect/stm-ref.ts @@ -0,0 +1,3 @@ +import { Effect, STM, TRef } from "effect"; +const p = STM.gen(function* () { const r = yield* TRef.make(20); yield* TRef.update(r, (n: number) => n + 22); return yield* TRef.get(r); }); +Effect.runPromise(STM.commit(p)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/stream-async-iterable.ts b/tests/fixtures/effect/stream-async-iterable.ts new file mode 100644 index 000000000..b2dd7c359 --- /dev/null +++ b/tests/fixtures/effect/stream-async-iterable.ts @@ -0,0 +1,4 @@ +import { Effect, Stream, Chunk } from "effect"; +async function* values() { yield 1; yield 2; } +const s = Stream.fromAsyncIterable(values(), (e) => String(e)); +Effect.runPromise(Stream.runCollect(s)).then((c) => console.log(Chunk.toReadonlyArray(c).join(","))); diff --git a/tests/fixtures/effect/stream-drain.ts b/tests/fixtures/effect/stream-drain.ts new file mode 100644 index 000000000..a482e5c08 --- /dev/null +++ b/tests/fixtures/effect/stream-drain.ts @@ -0,0 +1,2 @@ +import { Effect, Stream } from "effect"; +Effect.runPromise(Stream.runDrain(Stream.make(1, 2, 3))).then(() => console.log("drained")); diff --git a/tests/fixtures/effect/stream-effect.ts b/tests/fixtures/effect/stream-effect.ts new file mode 100644 index 000000000..a603df23b --- /dev/null +++ b/tests/fixtures/effect/stream-effect.ts @@ -0,0 +1,3 @@ +import { Effect, Stream, Chunk, pipe } from "effect"; +const s = pipe(Stream.fromEffect(Effect.succeed(21)), Stream.mapEffect((n: number) => Effect.succeed(n * 2))); +Effect.runPromise(Stream.runCollect(s)).then((c) => console.log(Chunk.toReadonlyArray(c).join(","))); diff --git a/tests/fixtures/effect/stream-error.ts b/tests/fixtures/effect/stream-error.ts new file mode 100644 index 000000000..acf254235 --- /dev/null +++ b/tests/fixtures/effect/stream-error.ts @@ -0,0 +1,3 @@ +import { Effect, Stream, Chunk } from "effect"; +const s = Stream.catchAll(Stream.fail("bad"), (s: string) => Stream.succeed("caught:" + s)); +Effect.runPromise(Stream.runCollect(s)).then((c) => console.log(Chunk.toReadonlyArray(c).join(","))); diff --git a/tests/fixtures/effect/stream-map-filter.ts b/tests/fixtures/effect/stream-map-filter.ts new file mode 100644 index 000000000..fab9656ca --- /dev/null +++ b/tests/fixtures/effect/stream-map-filter.ts @@ -0,0 +1,3 @@ +import { Effect, Stream, Chunk, pipe } from "effect"; +const s = pipe(Stream.fromIterable([1, 2, 3]), Stream.map((n: number) => n * 2), Stream.filter((n: number) => n > 2)); +Effect.runPromise(Stream.runCollect(s)).then((c) => console.log(Chunk.toReadonlyArray(c).join(","))); diff --git a/tests/fixtures/effect/stream-merge.ts b/tests/fixtures/effect/stream-merge.ts new file mode 100644 index 000000000..37c579baa --- /dev/null +++ b/tests/fixtures/effect/stream-merge.ts @@ -0,0 +1,2 @@ +import { Effect, Stream, Chunk } from "effect"; +Effect.runPromise(Stream.runCollect(Stream.merge(Stream.make(1), Stream.make(2)))).then((c) => console.log([...Chunk.toReadonlyArray(c)].sort().join(","))); diff --git a/tests/fixtures/effect/stream-sink.ts b/tests/fixtures/effect/stream-sink.ts new file mode 100644 index 000000000..6a7927c7e --- /dev/null +++ b/tests/fixtures/effect/stream-sink.ts @@ -0,0 +1,2 @@ +import { Effect, Stream, Sink } from "effect"; +Effect.runPromise(Stream.run(Stream.fromIterable([1, 2, 3]), Sink.sum)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/stream-take.ts b/tests/fixtures/effect/stream-take.ts new file mode 100644 index 000000000..eca723a87 --- /dev/null +++ b/tests/fixtures/effect/stream-take.ts @@ -0,0 +1,3 @@ +import { Effect, Stream, Chunk } from "effect"; +const s = Stream.take(Stream.iterate(1, (n: number) => n + 1), 3); +Effect.runPromise(Stream.runCollect(s)).then((c) => console.log(Chunk.toReadonlyArray(c).join(","))); diff --git a/tests/fixtures/effect/sync-value.ts b/tests/fixtures/effect/sync-value.ts new file mode 100644 index 000000000..369560366 --- /dev/null +++ b/tests/fixtures/effect/sync-value.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.sync(() => 42)).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/tap-first.ts b/tests/fixtures/effect/tap-first.ts new file mode 100644 index 000000000..430abe4fd --- /dev/null +++ b/tests/fixtures/effect/tap-first.ts @@ -0,0 +1,2 @@ +import { Effect } from "effect"; +Effect.runPromise(Effect.tap(Effect.succeed(42), (n: number) => Effect.sync(() => console.log("tap", n)))).then((n: number) => console.log(n)); diff --git a/tests/fixtures/effect/tracing-span.ts b/tests/fixtures/effect/tracing-span.ts new file mode 100644 index 000000000..1dd61d7ac --- /dev/null +++ b/tests/fixtures/effect/tracing-span.ts @@ -0,0 +1,3 @@ +import { Effect } from "effect"; +const p = Effect.withSpan(Effect.map(Effect.currentSpan, (s) => s.name), "operation"); +Effect.runPromise(p).then((s: string) => console.log(s)); diff --git a/tests/fixtures/npm/cases/2469-bundle-offender/main.ts b/tests/fixtures/npm/cases/2469-bundle-offender/main.ts index cb5db3f31..544d86de9 100644 --- a/tests/fixtures/npm/cases/2469-bundle-offender/main.ts +++ b/tests/fixtures/npm/cases/2469-bundle-offender/main.ts @@ -1,8 +1,6 @@ -// A TYPE-ONLY name has no JS value the inferred surface can carry: the -// import site reports against the package, and the offender attribution -// degrades exactly that package to the island with a note — never a -// failed gate (coverage pins this; there is no differential run). -import { look, GhostShape } from "gtghost"; +// Published type-only declarations check the consumer while runtime +// exports come from the package's shipped JavaScript. +import { look, type GhostShape } from "gtghost"; const g: GhostShape = { id: 4 }; console.log(look(), g.id); diff --git a/tests/fixtures/npm/cases/2469-bundle-offender/package.json b/tests/fixtures/npm/cases/2469-bundle-offender/package.json new file mode 100644 index 000000000..089153bcb --- /dev/null +++ b/tests/fixtures/npm/cases/2469-bundle-offender/package.json @@ -0,0 +1 @@ +{"type":"module"} diff --git a/tests/fixtures/self-hosting/ir-types.ts b/tests/fixtures/self-hosting/ir-types.ts index c2dd47c4c..12861cf3f 100644 --- a/tests/fixtures/self-hosting/ir-types.ts +++ b/tests/fixtures/self-hosting/ir-types.ts @@ -1,5 +1,5 @@ import { streamRooted, unionWideningTags, type IrClassAncestry } from "../../../packages/compiler/src/ir/analysis.js"; -import { BOOL, F64, STRING, VOID, isRefCounted, typeEquals, type IrType } from "../../../packages/compiler/src/ir/ir.js"; +import { BOOL, F64, STRING, VOID, canConvertToDyn, canDynCheckTo, isRefCounted, typeEquals, type IrRecordShape, type IrType, type IrUnionDef } from "../../../packages/compiler/src/ir/ir.js"; // These are the compiler's real type comparison and ownership queries. // Native callers pass recursive, nullable, and optional IR fields through @@ -88,3 +88,35 @@ const child: IrClassAncestry = { def: { name: "Child" }, base: stream }; const grandchild: IrClassAncestry = { def: { name: "Grandchild" }, base: child }; const unrelated: IrClassAncestry = { def: { name: "Unrelated" }, base: plain }; console.log("roots", streamRooted(plain), streamRooted(stream), streamRooted(child), streamRooted(grandchild), streamRooted(unrelated)); + +// Shared type graphs must remain bounded when these queries run inside +// the native compiler, including checked conversions beyond JSON data. +const shapes = new Map(); +const unions = new Map(); +let shared: IrType = { kind: "symbol" }; +for (let i = 0; i < 16; i++) { + const id = `r${i}`, unionId = `u${i}`; + shapes.set(id, { id, fields: [{ name: "left", type: shared }, { name: "right", type: shared }] }); + unions.set(unionId, { id: unionId, arms: [{ kind: "record", shapeId: id }, { kind: "undefinedT" }] }); + shared = { kind: "union", unionId }; +} +let lookups = 0; +function getShape(id: string): IrRecordShape | undefined { lookups++; return shapes.get(id); } +function getUnion(id: string): IrUnionDef | undefined { lookups++; return unions.get(id); } +console.log("box graph", canConvertToDyn(shared, getShape, getUnion), lookups < 1000); +lookups = 0; +console.log("check graph", canDynCheckTo(shared, getShape, getUnion), lookups < 1000); + +shapes.set("parent", { id: "parent", fields: [ + { name: "child", type: { kind: "record", shapeId: "child" } }, + { name: "unsupported", type: { kind: "jsval" } }, +] }); +shapes.set("child", { id: "child", fields: [ + { name: "parent", type: { kind: "record", shapeId: "parent" } }, + { name: "symbol", type: { kind: "symbol" } }, +] }); +const visiting = new Set(); +console.log("recursive failures", + canConvertToDyn({ kind: "record", shapeId: "parent" }, getShape, getUnion, visiting), + canConvertToDyn({ kind: "record", shapeId: "child" }, getShape, getUnion, visiting), +); diff --git a/tests/fixtures/self-hosting/lowering-context-cases.ts b/tests/fixtures/self-hosting/lowering-context-cases.ts index 069415847..efa27183d 100644 --- a/tests/fixtures/self-hosting/lowering-context-cases.ts +++ b/tests/fixtures/self-hosting/lowering-context-cases.ts @@ -132,8 +132,8 @@ function runContextCase(services: FrontendServices, entry: string): string[] { const caughtLocal = declareContextLocal(caught, "caught", CAUGHT, false, outerSymbol, source); const caughtChild = newFnCtx(true, null, null, VOID); const caughtResult = captureContextBinding([caught, caughtChild], outerSymbol, capture); - requireValue(caughtResult.error === "caught" && caughtResult.origin === caughtLocal, "catch capture did not report its boundary"); - requireValue(caughtLocal.boxed === undefined && caughtChild.captures?.length === 0, "catch refusal allocated storage"); + requireValue(caughtResult.error === null && caughtResult.origin === caughtLocal, "catch capture lost its source binding"); + requireValue(caughtLocal.boxed === true && caughtChild.captures?.length === 1, "catch capture did not allocate shared storage"); const plain = newFnCtx(false, null, null, VOID); const plainResult = captureContextBinding([root, plain], outerSymbol, capture); requireValue(plainResult.error === "plain" && plainResult.local === null, "plain function accepted captures"); diff --git a/tests/fixtures/self-hosting/opaque-class-view.ts b/tests/fixtures/self-hosting/opaque-class-view.ts index 654f030f1..c3eb2ad35 100644 --- a/tests/fixtures/self-hosting/opaque-class-view.ts +++ b/tests/fixtures/self-hosting/opaque-class-view.ts @@ -1,6 +1,7 @@ class Context { readonly values = new Map(); readonly members = new Set(); + readonly count = (...values: number[]): number => values.length; value = 7; bump(): number { this.values.set("value", ++this.value); return this.value; } } @@ -14,5 +15,6 @@ for (let i = 0; i < 3; i++) { } const restored = opaque as Context; if (restored !== original || restored.bump() !== 8 + i) throw new Error("capsule identity lost"); + if (restored.count(1, 2) !== 2) throw new Error("native rest callback lost"); } console.log(original.values.get("value"), original.members.size); diff --git a/tests/fixtures/self-hosting/union-conversions.ts b/tests/fixtures/self-hosting/union-conversions.ts index 3f33311e3..58d49f863 100644 --- a/tests/fixtures/self-hosting/union-conversions.ts +++ b/tests/fixtures/self-hosting/union-conversions.ts @@ -220,6 +220,7 @@ const errorFields = [ { name: "name", type: STRING }, { name: "message", type: STRING }, { name: "%code", type: STRING }, { name: "%cause", type: { kind: "dyn" } as IrType }, { name: "%causeEnumerable", type: BOOL }, + { name: "%messagePresent", type: BOOL }, ]; const classes: IrClassDef[] = [ { name: "%Error", runtime: true, fields: errorFields, loc }, diff --git a/tests/harness/__snapshots__/accessors.ts.txt b/tests/harness/__snapshots__/accessors.ts.txt index 20061b7ca..12508f3ad 100644 --- a/tests/harness/__snapshots__/accessors.ts.txt +++ b/tests/harness/__snapshots__/accessors.ts.txt @@ -54,13 +54,6 @@ accessors.ts:99:5 - error SC1090: compound assignment through 'super' (read and | ^~~~~~~~~~~ 100 | } -accessors.ts:105:3 - error SC1090: computed accessor names are not supported yet - - 104 | class Computed { - 105 | get ["q"](): number { - | ^~~~~~~~~~~~~~~~~~~~~ - 106 | return 1; - accessors.ts:119:15 - error SC1090: bound method references through 'super' (call 'super.m(...)' directly) is not supported yet 118 | probe(): number { @@ -94,11 +87,4 @@ accessors.ts:135:1 - error SC1090: constructing through a class value whose clas 134 | new DogKennel(); 135 | new WriteOnlyCell(); | ^~~~~~~~~~~~~~~~~~~ - 136 | new Computed(); - -accessors.ts:136:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - - 135 | new WriteOnlyCell(); - 136 | new Computed(); - | ^~~~~~~~~~~~~~ - 137 | \ No newline at end of file + 136 | new Computed(); \ No newline at end of file diff --git a/tests/harness/__snapshots__/assert-fences.ts.txt b/tests/harness/__snapshots__/assert-fences.ts.txt index 9a87c7fa1..eb32e6010 100644 --- a/tests/harness/__snapshots__/assert-fences.ts.txt +++ b/tests/harness/__snapshots__/assert-fences.ts.txt @@ -52,15 +52,6 @@ assert-fences.ts:28:29 - error SC2020: 'assert.deepStrictEqual of 'unknown' and hint: the unknown boundary cannot carry the Buffer-vs-Uint8Array brand Node compares — narrow the unknown side to a typed array and compare those -assert-fences.ts:29:29 - error SC2020: 'assert.deepStrictEqual of 'unknown' and 'Map' values' is part of the standard library types but has no scriptc lowering yet - - 28 | assert.deepStrictEqual(unk, new Uint8Array([1])); // bytes static side: the dyn copy cannot carry the brand - 29 | assert.deepStrictEqual(unk, new Map()); // not dyn-convertible - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 30 | - - hint: the static side must convert into the checked-dynamic tree (numbers, strings, booleans, null/undefined, boxable functions, or JSON-safe structures under the deep forms) — narrow the unknown side instead - assert-fences.ts:31:1 - error SC2020: 'assert.deepStrictEqual of 'number[]' and 'string[]' values' is part of the standard library types but has no scriptc lowering yet 30 | diff --git a/tests/harness/__snapshots__/catch-bindings.ts.txt b/tests/harness/__snapshots__/catch-bindings.ts.txt index fe6e1577c..21e0b17a5 100644 --- a/tests/harness/__snapshots__/catch-bindings.ts.txt +++ b/tests/harness/__snapshots__/catch-bindings.ts.txt @@ -1,10 +1,3 @@ -catch-bindings.ts:15:32 - error SC1090: closures capturing catch bindings (narrow into a typed local first) are not supported yet - - 14 | } catch (e) { - 15 | const probe = (): boolean => e instanceof Error; // capture by a closure - | ^ - 16 | console.log(probe()); - catch-bindings.ts:21:7 - error SC1090: 'instanceof' on a catch binding against the standalone class 'Standalone' (only classes in extends hierarchies carry the vtable the payload test needs) is not supported yet 20 | } catch (e) { diff --git a/tests/harness/__snapshots__/collection-views.ts.txt b/tests/harness/__snapshots__/collection-views.ts.txt index 1e6f9b9c3..fa6ec2946 100644 --- a/tests/harness/__snapshots__/collection-views.ts.txt +++ b/tests/harness/__snapshots__/collection-views.ts.txt @@ -37,38 +37,4 @@ collection-views.ts:16:14 - error SC1090: 'Map' values where 'Ma 15 | } 16 | console.log((new Map([[1, 2]]) as unknown as WordMap).has("word")); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 17 | - -collection-views.ts:20:7 - error SC2009: values of type 'TaggedSet' cannot be compiled: the record shape is supported, but its member 'forEach' has type '(callbackfn: (value: string, value2: string, set: ReadonlySet) => void, thisArg?: any) => void', which does not compile - - 19 | interface TaggedSet extends ReadonlySet { readonly label: string } - 20 | const tagged = new Set() as unknown as TaggedSet; - | ^~~~~~ - 21 | console.log(tagged.label); - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -collection-views.ts:21:13 - error SC2004: uses of 'tagged' inherit the blocker on its declaration - - 20 | const tagged = new Set() as unknown as TaggedSet; - 21 | console.log(tagged.label); - | ^~~~~~~~~~~~ - 22 | - - hint: the declaration of 'tagged' did not compile — fix the diagnostic reported there and these sites clear with it - -collection-views.ts:27:7 - error SC2009: values of type 'OverloadedSet' cannot be compiled: the record shape is supported, but its member 'has' has type '{ (value: string): boolean; (value: number): boolean; }', which does not compile - - 26 | } - 27 | const overloaded = new Set() as unknown as OverloadedSet; - | ^~~~~~~~~~ - 28 | console.log(overloaded.has(1)); - - hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') - -collection-views.ts:28:13 - error SC1090: method calls like 'overloaded.has' is not supported yet - - 27 | const overloaded = new Set() as unknown as OverloadedSet; - 28 | console.log(overloaded.has(1)); - | ^~~~~~~~~~~~~~~~~ - 29 | \ No newline at end of file + 17 | \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-dynamic-mix.txt b/tests/harness/__snapshots__/coverage-dynamic-mix.txt index 6b1bd4bb4..fbbb37236 100644 --- a/tests/harness/__snapshots__/coverage-dynamic-mix.txt +++ b/tests/harness/__snapshots__/coverage-dynamic-mix.txt @@ -1,13 +1,12 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts statements analyzed 9 - compile statically 4 (44%) + compile statically 5 (55%) - runs with --dynamic 4 sites (embeds a JS engine, ~620KB — static stays the default) - ×1 '__island_eval' requires the embedded dynamic engine, which this build does not include SC2010 - ×1 the '*' operator on 'any'-typed values runs in the embedded dynamic engine, which this build does not include SC2011 - ×1 '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include SC2012 - ×1 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include SC2012 + runs with --dynamic 3 sites (embeds a JS engine, ~620KB — static stays the default) + ×1 '__island_eval' requires the embedded dynamic engine, which this build does not include SC2010 + ×1 '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include SC2012 + ×1 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include SC2012 blockers: - ×1 loose equality (== and !=) SC1040 \ No newline at end of file + ×1 loose equality (== and !=) SC1040 \ No newline at end of file diff --git a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt index 0c163a9e9..6e4884a2b 100644 --- a/tests/harness/__snapshots__/dynamic-representable-types.ts.txt +++ b/tests/harness/__snapshots__/dynamic-representable-types.ts.txt @@ -16,24 +16,6 @@ dynamic-representable-types.ts:10:5 - error SC2011: values of type 'any[]' have hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) -dynamic-representable-types.ts:11:7 - error SC2011: values of type '{ workItem: any; width: string; }' have no static representation but run in the embedded dynamic engine, which this build does not include - - 10 | var evolving = []; // never used — the tsc-clean evolving-array shape - 11 | const rec = { workItem: {} as any, width: "10px" }; - | ^~~ - 12 | const fn: (v: any) => string = (v) => String(v); - - hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) - -dynamic-representable-types.ts:11:13 - error SC2011: values of type '{ workItem: any; width: string; }' have no static representation but run in the embedded dynamic engine, which this build does not include - - 10 | var evolving = []; // never used — the tsc-clean evolving-array shape - 11 | const rec = { workItem: {} as any, width: "10px" }; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 12 | const fn: (v: any) => string = (v) => String(v); - - hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) - dynamic-representable-types.ts:14:7 - error SC2011: values of type '[number, (string | undefined)?]' have no static representation but run in the embedded dynamic engine, which this build does not include 13 | const bare: any = 41; @@ -43,11 +25,11 @@ dynamic-representable-types.ts:14:7 - error SC2011: values of type '[number, (st hint: build with --dynamic to run these values in the embedded engine (adds ~620KB to the binary), or restate the type inside the static surface (https://scriptc.dev/limitations describes the boundary) -dynamic-representable-types.ts:15:25 - error SC2004: uses of 'rec' inherit the blocker on its declaration +dynamic-representable-types.ts:15:49 - error SC2004: uses of 'optTuple' inherit the blocker on its declaration 14 | const optTuple: [number, string?] = [1]; 15 | console.log(arr.length, rec.width, fn(1), bare, optTuple.length); - | ^~~~~~~~~ + | ^~~~~~~~~~~~~~~ 16 | export const marker: number = 1; - hint: the declaration of 'rec' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file + hint: the declaration of 'optTuple' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/json-dyn.ts.txt b/tests/harness/__snapshots__/json-dyn.ts.txt index 452eba3a3..4b0f60784 100644 --- a/tests/harness/__snapshots__/json-dyn.ts.txt +++ b/tests/harness/__snapshots__/json-dyn.ts.txt @@ -1,21 +1,3 @@ -json-dyn.ts:22:35 - error SC1101: converting typed values to 'unknown' is not supported yet - - 21 | // dyn representation. - 22 | const typedIntoUnknown: unknown = new Map(); - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 23 | function wantsUnknown(x: unknown): void {} - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet - -json-dyn.ts:24:14 - error SC1101: converting typed values to 'unknown' is not supported yet - - 23 | function wantsUnknown(x: unknown): void {} - 24 | wantsUnknown(new Map()); - | ^~~~~~~~~~~~~~~~~~~~~~~~~ - 25 | const stringifyClosure = JSON.stringify((x: number) => x + 1); - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet - json-dyn.ts:25:41 - error SC1090: JSON.stringify of '(number) => number' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet 24 | wantsUnknown(new Map()); diff --git a/tests/harness/__snapshots__/maps-surface.ts.txt b/tests/harness/__snapshots__/maps-surface.ts.txt index 66dad0656..e0d2aebe8 100644 --- a/tests/harness/__snapshots__/maps-surface.ts.txt +++ b/tests/harness/__snapshots__/maps-surface.ts.txt @@ -1,12 +1,3 @@ -maps-surface.ts:13:7 - error SC2020: 'MapIterator' is part of the standard library types but has no scriptc lowering yet - - 12 | // on the spot ([...m.keys()]); a stored iterator would need the protocol. - 13 | const ks = m.keys(); - | ^~ - 14 | const vs = m.values(); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - maps-surface.ts:13:12 - error SC2020: '.keys() outside an immediate array spread' is part of the standard library types but has no scriptc lowering yet 12 | // on the spot ([...m.keys()]); a stored iterator would need the protocol. @@ -16,15 +7,6 @@ maps-surface.ts:13:12 - error SC2020: '.keys() outside an immediate array spread hint: iterator objects have no lowering — drain it into an array where it is made: [...m.keys()] -maps-surface.ts:14:7 - error SC2020: 'MapIterator' is part of the standard library types but has no scriptc lowering yet - - 13 | const ks = m.keys(); - 14 | const vs = m.values(); - | ^~ - 15 | const es = m.entries(); - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - maps-surface.ts:14:12 - error SC2020: '.values() outside an immediate array spread' is part of the standard library types but has no scriptc lowering yet 13 | const ks = m.keys(); @@ -34,15 +16,6 @@ maps-surface.ts:14:12 - error SC2020: '.values() outside an immediate array spre hint: iterator objects have no lowering — drain it into an array where it is made: [...m.values()] -maps-surface.ts:15:7 - error SC2020: 'MapIterator<[string, number]>' is part of the standard library types but has no scriptc lowering yet - - 14 | const vs = m.values(); - 15 | const es = m.entries(); - | ^~ - 16 | - - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) - maps-surface.ts:15:12 - error SC2020: '.entries() outside an immediate array spread' is part of the standard library types but has no scriptc lowering yet 14 | const vs = m.values(); diff --git a/tests/harness/__snapshots__/mixins.ts.txt b/tests/harness/__snapshots__/mixins.ts.txt index 0a7fd1ee6..0129a3e19 100644 --- a/tests/harness/__snapshots__/mixins.ts.txt +++ b/tests/harness/__snapshots__/mixins.ts.txt @@ -16,6 +16,13 @@ mixins.ts:21:13 - error SC2004: uses of 'makeClass' inherit the blocker on its d hint: the declaration of 'makeClass' did not compile — fix the diagnostic reported there and these sites clear with it +mixins.ts:25:17 - error SC1090: this mixin call (its instantiation failed to collect — see the instantiation's own diagnostics) is not supported yet + + 24 | // sit under a mixin layer. + 25 | class E extends M(Error) {} + | ^~~~~~~~ + 26 | console.log(new E().m); + mixins.ts:25:19 - error SC1090: mixins over the builtin class 'Error' (its construction is libCall-shaped — only program classes compose) is not supported yet 24 | // sit under a mixin layer. @@ -71,6 +78,13 @@ mixins.ts:48:17 - error SC1090: mixin constructors whose rest parameter does any | ^~~~~~~~~~~~~~ 49 | super(...args); +mixins.ts:55:17 - error SC1090: this mixin call (its instantiation failed to collect — see the instantiation's own diagnostics) is not supported yet + + 54 | } + 55 | class S extends Sneaky(Base) {} + | ^~~~~~~~~~~~ + 56 | console.log(new S().argc); + mixins.ts:56:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet 55 | class S extends Sneaky(Base) {} @@ -85,6 +99,15 @@ mixins.ts:67:9 - error SC1090: extending computed expressions are not supported | ^~~~~~~~~~~~~~~~~~~ 68 | console.log(new L().l); +mixins.ts:67:17 - error SC2008: values of type '{ new (...args: any[]): Leaky.(Anonymous class); prototype: Leaky.(Anonymous class); } & typeof Base' cannot be compiled: this intersection resolves to no runtime shape + + 66 | } + 67 | class L extends Leaky(Base) {} + | ^~~~~~~~~~~ + 68 | console.log(new L().l); + + hint: restate the intersection as a single interface or type literal with the combined members; mixin-produced intersections compile where the mixin chain pins one instantiation + mixins.ts:68:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet 67 | class L extends Leaky(Base) {} diff --git a/tests/harness/__snapshots__/record-width.ts.txt b/tests/harness/__snapshots__/record-width.ts.txt index 64c701987..37be4df54 100644 --- a/tests/harness/__snapshots__/record-width.ts.txt +++ b/tests/harness/__snapshots__/record-width.ts.txt @@ -23,15 +23,6 @@ record-width.ts:27:13 - error SC1090: console.log of '{ m: ({ a: number; b: numb | ^~~~ 28 | -record-width.ts:33:38 - error SC1101: converting typed values to 'unknown' is not supported yet - - 32 | const withMap: WithMap = { m: new Map(), n: 1 }; - 33 | const bag: Record = withMap; - | ^~~~~~~ - 34 | console.log(bag); - - hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet - record-width.ts:40:18 - error SC2002: record shapes must match exactly or width-coerce: expected '{ a: boolean | undefined; [key: string]: boolean | string | undefined }', got '{ b: string }' — the expected field 'a' ('boolean | undefined') cannot take a runtime key collision from the 'boolean | string | undefined' signature slot 39 | const plain = { b: "hi" }; diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 8376ae8c1..efab86423 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -170,24 +170,6 @@ stdlib-fence.ts:52:1 - error SC2020: '.forEach with 2 arguments' is part of the hint: the thisArg parameter has no lowering — use an arrow function -stdlib-fence.ts:61:16 - error SC2020: 'JSON.stringify with replacer/space parameters' is part of the standard library types but has no scriptc lowering yet - - 60 | const width = 2; - 61 | const spaced = JSON.stringify(toStringify, null, width); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 62 | // The promise surface beyond await. - - hint: the serializer is type-directed — shape the value before stringifying - -stdlib-fence.ts:73:15 - error SC2020: 'Object.is over a dynamic operand' is part of the standard library types but has no scriptc lowering yet - - 72 | // unions needing narrowing. - 73 | const dynIs = Object.is(JSON.parse("1") as unknown, 1); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 74 | function pick(a: string | number, b: number | boolean): boolean { - - hint: validate/narrow the value first (strict equality's rule) — SameValue only differs from === on numbers (NaN, ±0) - stdlib-fence.ts:75:10 - error SC2020: 'Object.is over these union operands' is part of the standard library types but has no scriptc lowering yet 74 | function pick(a: string | number, b: number | boolean): boolean { diff --git a/tests/harness/__snapshots__/text-codec-value.ts.txt b/tests/harness/__snapshots__/text-codec-value.ts.txt index 33a36e22d..e64d6996b 100644 --- a/tests/harness/__snapshots__/text-codec-value.ts.txt +++ b/tests/harness/__snapshots__/text-codec-value.ts.txt @@ -32,13 +32,6 @@ text-codec-value.ts:11:1 - error SC2020: 'TextEncoder.encodeInto' is part of the hint: TextEncoder instances can be stored, passed, and captured; encode(string) and encode() compile, while encodeInto and other members have no static lowering -text-codec-value.ts:12:16 - error SC1090: a checked cast of 'unknown' to 'TextDecoder' (a dynamic value can only be validated against JSON-representable types: number, string, boolean, records, arrays, and unions of those) is not supported yet - - 11 | encoder.encodeInto("hello", new Uint8Array(10)); - 12 | const forged = JSON.parse("{}") as TextDecoder; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 13 | console.log(forged.decode()); - text-codec-value.ts:18:5 - error SC2020: 'uninitialized lexical codec bindings' is part of the standard library types but has no scriptc lowering yet 17 | var varEncoder = new TextEncoder(); diff --git a/tests/harness/__snapshots__/utility-types.ts.txt b/tests/harness/__snapshots__/utility-types.ts.txt index ea731dbc6..371215a65 100644 --- a/tests/harness/__snapshots__/utility-types.ts.txt +++ b/tests/harness/__snapshots__/utility-types.ts.txt @@ -41,18 +41,11 @@ utility-types.ts:20:13 - error SC1090: console.log of 'Date' values ('Date' valu | ^~~~ 21 | -utility-types.ts:22:7 - error SC2001: values of type 'Readonly' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown') +utility-types.ts:22:33 - error SC1101: converting typed values to 'unknown' is not supported yet 21 | 22 | const stamped: Readonly = new Date(0); - | ^~~~~~~ - 23 | console.log(stamped.getTime()); - -utility-types.ts:23:13 - error SC2020: 'Readonly.getTime' is part of the standard library types but has no scriptc lowering yet - - 22 | const stamped: Readonly = new Date(0); + | ^~~~~~~~~~~ 23 | console.log(stamped.getTime()); - | ^~~~~~~~~~~~~~~ - 24 | - hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) \ No newline at end of file + hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet \ No newline at end of file diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 8b8e29b36..2ce301ba3 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -70,6 +70,21 @@ async function compileAndRun(name: string, source: string, ext: "ts" | "cjs" = " } describe(`dynamic-boundary checks (scriptc-only${sanitize ? ", sanitized" : ""})`, () => { + test("promise and codec checked casts reject unrelated values", async () => { + const r = await compileAndRun("promise-codec-casts", ` + const value: unknown = {}; + try { const promise = value as Promise; console.log(typeof promise); } + catch (error) { console.log(error instanceof TypeError); } + try { const optional = value as Promise | undefined; console.log(typeof optional); } + catch (error) { console.log(error instanceof TypeError); } + try { const decoder = value as TextDecoder; console.log(decoder.decode()); } + catch (error) { console.log(error instanceof TypeError); } + `); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe("true\ntrue\ntrue\n"); + expect(r.stderr).toBe(""); + }); + test("wrong-typed field throws with the path", async () => { const r = await compileAndRun( "wrong-type", diff --git a/tests/harness/effect.test.ts b/tests/harness/effect.test.ts new file mode 100644 index 000000000..f24718876 --- /dev/null +++ b/tests/harness/effect.test.ts @@ -0,0 +1,46 @@ +import { execFile } from "node:child_process"; +import { globSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { basename, join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; +import { shardSelect } from "./shard.js"; + +const exec = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const fixtures = globSync(join(import.meta.dirname, "../fixtures/effect/*.ts")).sort(); +const cases = shardSelect(fixtures, (file) => basename(file)); + +async function run(command: string, args: string[]) { + try { + const { stdout, stderr } = await exec(command, args, { encoding: "utf8", timeout: 30_000 }); + return { stdout, stderr, status: 0 }; + } catch (error) { + const result = error as { code?: unknown; stdout?: string; stderr?: string }; + if (typeof result.code !== "number") throw error; + return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.code }; + } +} + +test.for(cases)("published Effect %s matches Node statically", async (entry) => { + const dir = await mkdtemp("/tmp/scriptc-effect-"); + try { + const reference = await run(process.execPath, ["--no-warnings", entry]); + expect(reference.status).toBe(0); + expect(reference.stdout.trim().length).toBeGreaterThan(0); + const result = await compile(entry, { + outDir: dir, outPath: join(dir, "program"), backend: "llvm", dynamic: false, + npmStatic: ["effect", "fast-check", "pure-rand"], sanitize, + }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const actual = await run(result.binaryPath, []); + if (sanitize) actual.stderr = actual.stderr.split("\n").filter((line) => + !line.startsWith("scriptc RC audit skipped:") && + !/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext/.test(line), + ).join("\n"); + expect(actual).toEqual(reference); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}, 600_000); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 16a7ffb3e..c0e468c58 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -217,61 +217,17 @@ console.log(two(1, 2, loud())); ); }); - test("Array.from fences an unsupported callback result before emission (JS lane)", async () => { - // Callback-driven producers learn their result element from the - // lowered callback, bypassing mapType's ordinary T[] gate. A - // JavaScript Date values use checked storage, whose callback result - // still has no static array element type on this producer path. - const r = await compileAndRun( - "array-from-length-elements", - `const arr = Array.from({ length: 2 }, () => new Date(0)); -console.log(arr.length); -`, - "js", - ); - expect(r.exitCode).toBe(1); - expect(r.stdout).toBe(""); - expect(r.stderr).toMatch( - /^Uncaught Error: 'Array\.from\(\{ length \}, mapper\)' with a callback returning 'unknown'-typed values .* \[SC1090 at .*array-from-length-elements\.js:1\]\n$/, - ); - }); - test.each([ - { - label: "flatMap", - name: "flatmap-elements", - producer: "'.flatMap()'", - result: "'unknown'-typed values \\(the result array has no static element type — annotate the callback's return\\)", - line: 1, - source: `const arr = [1].flatMap(() => new Date(0)); -console.log(arr.length); -`, - }, - { - label: "tuple map", - name: "tuple-map-elements", - producer: "'.map()'", - result: "'unknown'-typed values \\(the result array has no static element type — annotate the callback's return\\)", - line: 3, - source: `/** @type {[number]} */ + { label: "Array.from", name: "array-from-length-elements", source: `const arr = Array.from({ length: 2 }, () => new Date(0));` }, + { label: "flatMap", name: "flatmap-elements", source: `const arr = [1].flatMap(() => new Date(0));` }, + { label: "tuple map", name: "tuple-map-elements", source: `/** @type {[number]} */ const tuple = [1]; -const arr = tuple.map(() => new Date(0)); -console.log(arr.length); -`, - }, - ])("$label fences an unsupported callback result before emission (JS lane)", async ({ name, producer, result, line, source }) => { - // Both paths construct the ordinary map helper from the lowered - // callback return type. They must share Array.from/.map's frontend - // fence rather than leave the validator to report an internal error. - const r = await compileAndRun(name, source, "js"); - expect(r.exitCode).toBe(1); - expect(r.stdout).toBe(""); - const quotedProducer = producer.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - expect(r.stderr).toMatch( - new RegExp( - `^Uncaught Error: ${quotedProducer} with a callback returning ${result} are not supported yet \\[SC1090 at .*${name}\\.js:${line}\\]\\n$`, - ), - ); +const arr = tuple.map(() => new Date(0));` }, + ])("$label preserves checked callback results (JS lane)", async ({ name, source }) => { + const program = source + "\nconsole.log(arr.length, arr[0].getTime());\n"; + const r = await compileAndRun(name, program, "js"); + const reference = await execFileAsync(process.execPath, ["--eval", program], { encoding: "utf8" }); + expect(r).toEqual({ stdout: reference.stdout, stderr: reference.stderr, exitCode: 0 }); }); test("extending a property-assigned class ABOVE its assignment is a named deferred fence (JS lane)", async () => { diff --git a/tests/harness/native-weak-collections.test.ts b/tests/harness/native-weak-collections.test.ts index f5b08c042..4dc85d6e9 100644 --- a/tests/harness/native-weak-collections.test.ts +++ b/tests/harness/native-weak-collections.test.ts @@ -56,7 +56,8 @@ function check(value: any): void { check(record); check(array); check(tuple); -check(instance); +console.log(map.set(instance, 3) === map, map.get(instance)); +console.log(set.add(instance) === set, set.has(instance)); console.log("after"); `); const result = await compile(entry, { @@ -67,7 +68,8 @@ console.log("after"); if (!result.ok) return; const child = await exec(result.binaryPath); expect(child.stdout).toBe([ - ...Array(8).fill("Weak collection keys of this native reference type have no weak lifetime lowering"), + ...Array(6).fill("Weak collection keys of this native reference type have no weak lifetime lowering"), + "true 3", "true true", "after", "", ].join("\n")); expect(child.stderr).toBe(""); diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index dd7ad3019..bffea47a3 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -924,22 +924,21 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { ]); }, 120_000); - // 2469: a TYPE-ONLY surface name (an interface) has no JS value the - // inferred surface can carry — the import-site SC0001 NAMES the package, - // and the consumer-anchored attribution degrades exactly it to the - // island with the note, never a failed gate. Explicit opt-ins degrade - // like auto's: the ratified bundle-shape behavior. - test("a consumer-anchored surface break degrades the named package with a note", () => { + test("published type-only declarations coexist with a static runtime bundle", async () => { const entry = join(fixturesRoot, "npm/cases/2469-bundle-offender/main.ts"); const { coverage } = analyze(entry, { npmStatic: ["gtghost"] }); expect(coverage.preflightFailed).toBe(false); expect(coverage.npmStatic).toEqual([ { package: "gtghost", - status: "fallback", - detail: expect.stringContaining("inferred export surface breaks 1 import site") as string, + status: "static", }, ]); + const binary = await buildStatic(entry, ["gtghost"]); + const [reference, actual] = await Promise.all([runBinary(process.execPath, [entry]), runBinary(binary, [])]); + expect(actual.stdout).toEqual(reference.stdout); + expect(comparableStderr(actual.stderr)).toEqual(reference.stderr); + expect(actual.exitCode).toBe(reference.exitCode); }, 120_000); // The build-transform-marker relaxation: a getter-table bundle with its diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index fe25fb4df..95bcdbe54 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -181,7 +181,7 @@ const PROBES: Probe[] = [ id: "stdlib.headers.symbol.iterator", source: '/// \nfunction f([first]: Headers): void {\n void first;\n}\nvoid f;\n', }, - { id: "diagnostic.sc2011", source: "const y: any = 1;\nconst z = y * 2;\nconsole.log(0);\n" }, + { id: "diagnostic.sc2011", source: "const y: any = 1;\nconst { z } = y;\nconsole.log(0);\n" }, // status unsupported — refused with the entry's code { id: "stdlib.abort-signal.constructor",