From a3feec9b3554299a7672498189817c965e11da89 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 04:11:02 -0500 Subject: [PATCH 01/67] Add ThreadSlots, per-thread values keyed by objects, to @scriptc/experimental ThreadSlots holds one value per key and thread, like a WeakMap that each thread has its own copy of; under Node.js it is a WeakMap per name and thread. Setting a value never writes the key, so values can be set on published keys. Classes used as keys carry a 32-bit slot word after their header words, assigned on the first set for a key (with a compare-and-swap for a published key) and never reused. Each family has one block per thread that maps slot indices to values through pages of 1024 entries. get and has are inline reads of the slot word, the page and the entry; set and delete call scr_slots.c, which holds one reference to each value. A key class's free paths release the key's values, and thread exit releases the rest. --- benchmarks/runtime/README.md | 1 + benchmarks/runtime/workloads.json | 20 ++ benchmarks/runtime/workloads/thread-slots.ts | 122 +++++++++ docs/content/docs/experimental.mdx | 93 ++++++- docs/test/fixtures/migration-baseline.json | 16 ++ .../compiler/src/backend/cycle-mutability.ts | 12 +- packages/compiler/src/backend/llvm/classes.ts | 64 ++++- packages/compiler/src/backend/llvm/emitter.ts | 15 ++ .../src/backend/llvm/reference-writes.ts | 7 +- packages/compiler/src/backend/llvm/regions.ts | 4 +- .../src/backend/llvm/stack-objects.ts | 4 + .../src/backend/llvm/thread-slots.test.ts | 142 ++++++++++ .../compiler/src/backend/llvm/thread-slots.ts | 170 ++++++++++++ .../src/backend/native/runtime-inputs.ts | 1 + .../src/frontend/experimental-import.ts | 33 ++- .../src/frontend/lowering/lower-classes.ts | 3 + .../src/frontend/lowering/lower-exprs.ts | 8 + .../compiler/src/frontend/lowering/lowerer.ts | 21 +- .../src/frontend/lowering/thread-slots.ts | 254 ++++++++++++++++++ packages/compiler/src/frontend/type-mapper.ts | 16 +- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 18 +- packages/compiler/src/ir/validate.ts | 40 +++ .../corpus/5727-thread-slots-basics.ts.json | 6 + .../corpus/5728-thread-slots-workers.ts.json | 6 + .../corpus/5729-thread-slots-values.ts.json | 6 + .../diagnostics/thread-slots-fences.ts.json | 6 + packages/experimental/README.md | 45 +++- packages/experimental/index.d.ts | 21 ++ packages/experimental/index.js | 38 +++ packages/experimental/package.json | 2 +- .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_runtime.h | 1 + packages/runtime/src/scr_slots.c | 145 ++++++++++ packages/runtime/src/scr_slots.h | 73 +++++ packages/runtime/src/scr_slots.test.c | 113 ++++++++ packages/runtime/src/scr_slots.test.ts | 52 ++++ tests/corpus/5727-thread-slots-basics.ts | 85 ++++++ tests/corpus/5728-thread-slots-workers.ts | 107 ++++++++ tests/corpus/5729-thread-slots-values.ts | 89 ++++++ tests/diagnostics/thread-slots-fences.ts | 29 ++ .../__snapshots__/thread-slots-fences.ts.txt | 62 +++++ tests/harness/corpus-costs.json | 6 + tests/harness/wasi-differential.test.ts | 3 + 45 files changed, 1934 insertions(+), 28 deletions(-) create mode 100644 benchmarks/runtime/workloads/thread-slots.ts create mode 100644 packages/compiler/src/backend/llvm/thread-slots.test.ts create mode 100644 packages/compiler/src/backend/llvm/thread-slots.ts create mode 100644 packages/compiler/src/frontend/lowering/thread-slots.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json create mode 100644 packages/runtime/src/scr_slots.c create mode 100644 packages/runtime/src/scr_slots.h create mode 100644 packages/runtime/src/scr_slots.test.c create mode 100644 packages/runtime/src/scr_slots.test.ts create mode 100644 tests/corpus/5727-thread-slots-basics.ts create mode 100644 tests/corpus/5728-thread-slots-workers.ts create mode 100644 tests/corpus/5729-thread-slots-values.ts create mode 100644 tests/diagnostics/thread-slots-fences.ts create mode 100644 tests/harness/__snapshots__/thread-slots-fences.ts.txt diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index ed9ee0532..a3c8c73b9 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -17,6 +17,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | +| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | | `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index c0d643a58..1526b7ec9 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -127,6 +127,26 @@ "100000" ] }, + { + "name": "thread-slots", + "entry": "workloads/thread-slots.ts", + "args": [ + "slots", + "8", + "50000", + "20" + ] + }, + { + "name": "thread-slots-cache", + "entry": "workloads/thread-slots.ts", + "args": [ + "cache", + "8", + "50000", + "20" + ] + }, { "name": "atomics-contended", "entry": "workloads/atomics-contended.ts", diff --git a/benchmarks/runtime/workloads/thread-slots.ts b/benchmarks/runtime/workloads/thread-slots.ts new file mode 100644 index 000000000..9791b5a1a --- /dev/null +++ b/benchmarks/runtime/workloads/thread-slots.ts @@ -0,0 +1,122 @@ +// Per-thread state for shared objects: worker threads read the same +// published type graph from an InternTable and keep per-thread state for +// each type they visit (a resolved flag and a visit count), then walk the +// graph many times. Arguments: the store ("slots" for a ThreadSlots family, +// "cache" for a direct-mapped cache by id in front of a Map by id, the +// layout programs use without slots), the thread count, the type count and +// the number of walks. Under Node.js both stores are per-thread. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Type { + readonly id: number; + readonly members: readonly Type[]; + constructor(id: number, members: readonly Type[]) { + this.id = id; + this.members = members; + } +} + +class TypeState { + resolved = false; + visits = 0; +} + +const types = new InternTable("bench.slot-types"); +const states = new ThreadSlots("bench.type-states"); + +const CACHE = 1024; +const cacheIds: number[] = new Array(CACHE).fill(-1); +const cacheStates: (TypeState | undefined)[] = new Array(CACHE).fill( + undefined, +); +const byId = new Map(); + +function slotState(t: Type): TypeState { + let state = states.get(t); + if (state === undefined) { + state = new TypeState(); + states.set(t, state); + } + return state; +} + +function cachedState(t: Type): TypeState { + const slot = t.id & (CACHE - 1); + if (cacheIds[slot] === t.id) return cacheStates[slot]!; + let state = byId.get(t.id); + if (state === undefined) { + state = new TypeState(); + byId.set(t.id, state); + } + cacheIds[slot] = t.id; + cacheStates[slot] = state; + return state; +} + +function typeOf(id: number): Type { + return types.getOrInsertComputed(id, () => { + const members: Type[] = []; + if (id >= 3) members.push(typeOf(Math.floor(id / 3))); + if (id >= 7) members.push(typeOf(id - 7)); + return new Type(id, members); + }); +} + +function run(useSlots: boolean, count: number, walks: number): string { + const all: Type[] = []; + for (let id = 0; id < count; id++) all.push(typeOf(id)); + // Visits follow member edges from a pseudo-random start, as a checker's + // relation checks do: neighbours are often visited together. + let seed = 1; + let resolved = 0; + let visits = 0; + for (let w = 0; w < walks; w++) { + for (let i = 0; i < count; i++) { + seed = (seed * 48271) % 2147483647; + let t = all[seed % count]!; + for (let depth = 0; depth < 4; depth++) { + const state = useSlots ? slotState(t) : cachedState(t); + if (!state.resolved) { + state.resolved = true; + resolved++; + } + state.visits++; + visits++; + if (t.members.length === 0) break; + t = t.members[depth % t.members.length]!; + } + } + } + return `${resolved} resolved, ${visits} visits`; +} + +if (isMainThread) { + const store = process.argv[2] ?? "slots"; + const threads = Number(process.argv[3] ?? "8"); + const count = Number(process.argv[4] ?? "50000"); + const walks = Number(process.argv[5] ?? "20"); + const results: string[] = []; + let pending = threads; + for (let i = 0; i < threads; i++) { + const worker = new Worker(new URL(import.meta.url), { + workerData: { useSlots: store === "slots", count, walks }, + }); + worker.on("message", (text: string) => { + results[i] = text; + }); + worker.on("exit", () => { + if (--pending === 0) { + const same = results.every((r) => r === results[0]); + console.log(`${threads} threads: ${results[0]}, same in every thread ${same}`); + } + }); + } +} else { + const { useSlots, count, walks } = workerData as { + useSlots: boolean; + count: number; + walks: number; + }; + parentPort!.postMessage(run(useSlots, count, walks)); +} diff --git a/docs/content/docs/experimental.mdx b/docs/content/docs/experimental.mdx index ac5891ec9..e7ab2d194 100644 --- a/docs/content/docs/experimental.mdx +++ b/docs/content/docs/experimental.mdx @@ -183,8 +183,99 @@ No lock is held while a callback runs. Two threads that miss the same key at the 2. It does not compare identities or ids across threads. 3. A thread does not depend on another thread's inserts: `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js. -State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. +State that a thread computes about a shared value later (lazily resolved members, caches) belongs in thread-local state, such as a [`ThreadSlots`](#threadslots) family, a module-level `Map` keyed by the value or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. ### Memory Lookups take no lock and write nothing. Inserts lock one of 64 segments of the table for a probe and a few stores. Stored values are immortal in scriptc builds: they stay allocated until the process exits, including a value a callback computed for a key that another thread stored first. Programs that never construct an `InternTable` do not run any of its code. + +## ThreadSlots + +Threads that share values through an `InternTable` often compute more about each shared value later: resolved members, cached results, flags. That state differs per thread, so it cannot live in the shared value, which is immutable. A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares. + +```ts:slots.ts +import { Worker, isMainThread, parentPort, threadId } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Shape { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +class ShapeState { + uses = 0; + readonly thread: number; + constructor(thread: number) { + this.thread = thread; + } +} + +const shapes = new InternTable("example.slot-shapes"); +// Module scope: every thread that evaluates this line opens the same family. +const states = new ThreadSlots("example.shape-states"); + +function use(name: string): ShapeState { + const shape = shapes.getOrInsertComputed(name, () => new Shape(name)); + let state = states.get(shape); + if (state === undefined) { + state = new ShapeState(threadId); + states.set(shape, state); + } + state.uses++; + return state; +} + +if (isMainThread) { + for (let i = 0; i < 2; i++) { + new Worker(new URL(import.meta.url)).on("message", (text: string) => console.log(text)); + } +} else { + use("square"); + const state = use("square"); + parentPort!.postMessage(`worker: ${state.uses} uses, own state ${state.thread === threadId}`); +} +``` + +In the scriptc executable both workers read the same `Shape` object from the table, and each keeps its own `ShapeState` for it. Both runtimes print the same: + +```console +$ node slots.ts +worker: 2 uses, own state true +worker: 2 uses, own state true +$ scriptc build slots.ts -o slots >/dev/null +$ ./slots +worker: 2 uses, own state true +worker: 2 uses, own state true +``` + +### Slot methods + +
+
new ThreadSlots<K, V>(name)
+
Opens the family called name. Within one thread every construction with the same name refers to the same family.
+
slots.get(key), slots.has(key)
+
Read the calling thread's value for key.
+
slots.set(key, value)
+
Sets the calling thread's value for key and returns the slots. It never writes the key, so it works on published keys.
+
slots.delete(key)
+
Removes the calling thread's value for key and returns whether there was one.
+
+ +Under Node.js each family is a `WeakMap` per thread. + +### Keys and values + +scriptc requires: + +- the name to be a string literal, and every construction of one name to use the same type arguments; +- the key type to be a class declared in the program; keys may be instances of its subclasses; +- the value type to be a class, an object type, an array, a `Map` or a `Set`; +- the methods to be called through a module-level `const` initialized with `new ThreadSlots("name")`, as in the example. A family's storage is found by its name when the program is compiled. + +### Slot cost + +Each instance of a key class, and of every class in its hierarchy, carries a 4-byte slot index, 0 until a value is set for it. The first `set` for a key assigns the index; indices are never reused. `get` and `has` read the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family stores values in pages of 1024 entries, allocated as the thread sets values in them. + +A family holds a reference to each of the thread's values. A value is released when it is replaced or deleted, when its key is freed, and when the thread exits. Keys that are freed with a [`LifetimeRegion`](#lifetimeregion) keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. Programs that never construct a `ThreadSlots` do not run any of its code, and their classes have no slot index. diff --git a/docs/test/fixtures/migration-baseline.json b/docs/test/fixtures/migration-baseline.json index 6aaab5f70..1b99faf9a 100644 --- a/docs/test/fixtures/migration-baseline.json +++ b/docs/test/fixtures/migration-baseline.json @@ -296,6 +296,22 @@ { "text": "Memory", "id": "memory" + }, + { + "text": "ThreadSlots", + "id": "threadslots" + }, + { + "text": "Slot methods", + "id": "slot-methods" + }, + { + "text": "Keys and values", + "id": "keys-and-values" + }, + { + "text": "Slot cost", + "id": "slot-cost" } ] }, diff --git a/packages/compiler/src/backend/cycle-mutability.ts b/packages/compiler/src/backend/cycle-mutability.ts index c7d476527..79993fdff 100644 --- a/packages/compiler/src/backend/cycle-mutability.ts +++ b/packages/compiler/src/backend/cycle-mutability.ts @@ -97,16 +97,20 @@ export function computeCycleMutability( // can store; a commit rewrites every field, reference fields possibly // with fresh conversions). Without any such store in the program, // crossing into checked-dynamic code writes nothing. FFI calls and - // intrinsics other than publish (which only marks objects immortal) and - // the intern-table operations (which store published values) stay - // conservative. + // intrinsics other than publish (which only marks objects immortal), the + // intern-table operations (which store published values) and ThreadSlots + // reads stay conservative. const dynStores = dynamicFieldStores(mod); const dynamicStores = dynStores.all || dynStores.names.size > 0; result.emptyPropertyBags = !dynamicStores; const boundaryWrites = (e: IrExpr): boolean => dynamicStores || e.kind === "ffiCall" || - (e.kind === "intrinsic" && e.name !== "threads.publish" && !e.name.startsWith("itable.")); + (e.kind === "intrinsic" && + e.name !== "threads.publish" && + !e.name.startsWith("itable.") && + e.name !== "slots.get" && + e.name !== "slots.has"); // Types that can become checked-dynamic values: mark their whole closure. const exposedTypes = new Set(); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 8a13d83ad..02036e4d9 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -75,6 +75,10 @@ export class LlClassMeta { membership: { pre: number; post: number }[] = []; /** Type-only references retain layouts without retaining construction. */ constructorRetained = false; + /** The hierarchy holds ThreadSlots keys (IrModule.threadSlotKeys): every + * member's struct carries the i32 slot word before its fields + * (classSlotWordIndex). */ + threadSlot = false; constructor(readonly def: IrClassDef) { this.root = this; @@ -116,6 +120,12 @@ export function buildClassGraph( if (meta.base === null) number(meta, meta); meta.hierarchy = meta.base !== null || meta.children.length > 0; } + const slotRoots = new Set(); + for (const key of mod.threadSlotKeys ?? []) { + const meta = metaMap.get(key); + if (meta !== undefined) slotRoots.add(meta.root); + } + for (const meta of metaMap.values()) meta.threadSlot = slotRoots.has(meta.root); const declares = (m: LlClassMeta, method: string): boolean => m.def.methods?.includes(method) ?? false; const declaredBelow = (m: LlClassMeta, method: string): boolean => @@ -323,16 +333,31 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null { return RUNTIME_PREFIXES.get(meta.root.def.name) ?? null; } -/** The GEP index where a class's own fields start: rc at 0, the vtable - * word at 1 on hierarchy members, then a runtime prefix (state + display - * name) on classes rooted at a runtime base, then the stream-state slot - * on stream-rooted ones. */ -function fieldBase(meta: LlClassMeta): number { +/** The GEP index after a class's header words: rc at 0, the vtable word + * at 1 on hierarchy members, then a runtime prefix (state + display name) + * on classes rooted at a runtime base, then the stream-state slot on + * stream-rooted ones. */ +function headerEnd(meta: LlClassMeta): number { if (!meta.hierarchy) return 1; if (streamRooted(meta)) return 5; return runtimePrefixOf(meta) ? 4 : 2; } +/** The GEP index of the ThreadSlots slot word (an i32) in a class whose + * hierarchy holds ThreadSlots keys: right after the header words, so every + * member of the hierarchy has it at the same offset. */ +export function classSlotWordIndex(meta: LlClassMeta): number { + if (!meta.threadSlot) + throw new InternalCompilerError(`llvm emitter bug: class ${meta.def.name} has no slot word`); + return headerEnd(meta); +} + +/** The GEP index where a class's own fields start: after the header words + * and the slot word. */ +export function classFieldBase(meta: LlClassMeta): number { + return headerEnd(meta) + (meta.threadSlot ? 1 : 0); +} + /** Append the lexical environment after inherited fields to preserve base layouts. */ export function classEnvironmentIndex(meta: LlClassMeta): number { return classFieldIndex(meta, `%classEnvironment:${meta.def.name}`).index; @@ -346,7 +371,28 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb throw new InternalCompilerError( `llvm emitter bug: unknown field ${field} on class ${meta.def.name}`, ); - return { index: fieldBase(meta) + idx, type: meta.def.fields[idx]!.type }; + return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type }; +} + +/** Lines that release the ThreadSlots values of the object `%o` before it + * is freed: its slot word is 0 unless a value was ever set for it. */ +function slotForgetLines( + host: ClassHost, + struct: string, + meta: LlClassMeta, + tag: string, +): string[] { + host.declare(`declare void @scr_slots_forget(i32)`); + return [ + ` %${tag}p = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${classSlotWordIndex(meta)}`, + ` %${tag} = load i32, ptr %${tag}p, align 4`, + ` %${tag}.set = icmp ne i32 %${tag}, 0`, + ` br i1 %${tag}.set, label %${tag}.forget, label %${tag}.done`, + `${tag}.forget: ; release its ThreadSlots values`, + ` call void @scr_slots_forget(i32 %${tag})`, + ` br label %${tag}.done`, + `${tag}.done:`, + ]; } /** What class emission needs beyond ShapeHost: interned unit instances for @@ -483,7 +529,7 @@ export function emitClassShapes( ? ["ptr", "ptr", "ptr"] : ["ptr"] : []; - const members = [...prefix, ...fieldTys]; + const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys]; typeDefs.push( `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + `; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, @@ -569,7 +615,7 @@ export function emitClassShapes( const traced = host.tracedShapes.has(`object:${cls.name}`); const runtimePrefix = runtimePrefixOf(meta); const isStreamRooted = streamRooted(meta); - const fieldIndex = (i: number): number => fieldBase(meta) + i; + const fieldIndex = (i: number): number => classFieldBase(meta) + i; // Teardown, trace and gcFree see each slot's storage type: a nullable // class field holds the instance pointer (NULL for the unit arm), and // every class retain/release/visit entry point is NULL-tolerant. @@ -640,6 +686,7 @@ export function emitClassShapes( }); if (runtimePrefix) lines.push(...regCall("td", runtimePrefix.drop, "")); if (isStreamRooted) lines.push(...stCall("tds", "scr_stream_st_release", "")); + if (meta.threadSlot) lines.push(...slotForgetLines(host, struct, meta, "tdw")); lines.push(...emitObjectFree(host, traced)); }; @@ -841,6 +888,7 @@ export function emitClassShapes( ` call void ${releaseSym(host, f.type)}(ptr %v${i}) ; ${llvmCommentText(f.name)} (acyclic)`, ); }); + if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw")); gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``); defs.push(...gf); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index bd9a4b883..6adaa4678 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1,6 +1,7 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"; import { emitStringInputs } from "./string-lifetimes.js"; import { emitLlvmLayouts } from "./layouts.js"; +import { emitThreadSlotsIntrinsic, threadSlotsBlockDefinitions } from "./thread-slots.js"; import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js"; import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; @@ -834,6 +835,8 @@ export class LlEmitter { /** Constant-key reads on checked-dynamic objects, each with its own * zero-initialized ScrDynReadIc (scr_runtime.h). */ private dynReadIcSites = 0; + /** ThreadSlots blocks by family name (thread-slots.ts). */ + private readonly threadSlotsBlocks = new Map(); /** May-throw analysis (computeMayThrow): pending * checks are emitted only after calls that can actually raise. */ readonly mayThrow: Set; @@ -2369,6 +2372,7 @@ export class LlEmitter { { const tl = this.mod.workers === true || this.mod.lib?.threadInstances === true ? "thread_local " : ""; + out.push(...threadSlotsBlockDefinitions(this.threadSlotsBlocks, tl !== "")); for (let site = 0; site < this.dynReadIcSites; site++) out.push( `@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${this.sizeType}, ${this.sizeType}, ptr } zeroinitializer`, @@ -9816,9 +9820,20 @@ export class LlEmitter { ) return this.emitRegionIntrinsic(e); if (e.name.startsWith("itable.")) return this.emitInternTableIntrinsic(e); + if (e.name.startsWith("slots.")) return emitThreadSlotsIntrinsic(this, e); return emitIntrinsicExpr(this, e); } + /** The ScrSlots block of the ThreadSlots family `name`. */ + threadSlotsBlock(name: string): string { + let sym = this.threadSlotsBlocks.get(name); + if (sym === undefined) { + sym = `@sc_slots_${this.threadSlotsBlocks.size}`; + this.threadSlotsBlocks.set(name, sym); + } + return sym; + } + /** InternTable's runtime calls (scr_itable.c). Table and entry handles * are f64 values carrying pointer bits; entry values are 64-bit slots * (double bits, a bool, or a pointer to an immortal value) that the diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 33110eaa4..317104400 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -468,11 +468,14 @@ export class ReferenceWrites { return; case "intrinsic": // A TDZ test reads; publication only makes objects immortal; intern - // tables store published values and write no script object. + // tables store published values and write no script object; + // ThreadSlots reads write nothing. if ( e.name !== "module.tdzCheck" && e.name !== "threads.publish" && - !e.name.startsWith("itable.") + !e.name.startsWith("itable.") && + e.name !== "slots.get" && + e.name !== "slots.has" ) sink.all(); return; diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index 41f67c0cb..fbd41a3f7 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -36,7 +36,7 @@ import { mangleRecordStruct, } from "../mangle.js"; import { threadHotAddressLines } from "./alloc.js"; -import type { LlClassMeta } from "./classes.js"; +import { classFieldBase, type LlClassMeta } from "./classes.js"; import { llvmCommentText } from "./common.js"; import type { NullableRefFields } from "./nullable-fields.js"; import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js"; @@ -84,7 +84,7 @@ export class LifetimeRegions { /** The reference-holding fields; inline scalar unions own nothing. */ private classFields(def: IrClassDef, meta: LlClassMeta): Field[] { - const base = meta.hierarchy ? 2 : 1; + const base = classFieldBase(meta); return def.fields.flatMap((f, i) => this.host.inlineUnions.classField(def.name, f.name) ? [] diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index c03c7fb44..8183ce920 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -23,6 +23,10 @@ export type StackObjectSource = IrExpr & { kind: "recordLit" | "new" | "call" }; * (record-results.ts). The caller proves that every use of the value only * projects it. */ export function canStackObject(host: LlvmEmitterContext, e: IrExpr): e is StackObjectSource { + // A ThreadSlots key keeps its slot index for its lifetime and has its + // values released when it is freed: it lives on the heap. + if (e.type.kind === "object" && host.classMeta.get(e.type.className)?.threadSlot === true) + return false; // A producer with a result-slot entry builds its fresh result in the // caller's storage. if (e.kind === "call") return host.fnByName.has(resultEntryName(e.callee)); diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts new file mode 100644 index 000000000..c7260b9fe --- /dev/null +++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts @@ -0,0 +1,142 @@ +import { expect, test } from "vitest"; +import { BOOL, F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js"; +import { validateModule } from "../../ir/validate.js"; +import { emitLlvmModule } from "./emitter.js"; +import { mangleClassStruct } from "../mangle.js"; + +const loc = { file: "thread-slots.ts", start: 0, end: 1 }; +const slotsType: IrType = { kind: "object", className: "%ThreadSlots" }; +const node: IrType = { kind: "object", className: "Node" }; +const leaf: IrType = { kind: "object", className: "Leaf" }; +const plain: IrType = { kind: "object", className: "Plain" }; +const state: IrType = { kind: "object", className: "State" }; +const maybeState: IrType = { kind: "union", unionId: "u0" }; + +/** A key hierarchy (Node, Leaf), a class that is not a key (Plain), and the + * value class (State), with `slots` a module global of the slots class. */ +function module(body: IrExpr[]): IrModule { + const cls = (name: string, fields: [string, IrType][], base?: string) => ({ + name, + ...(base !== undefined ? { base } : {}), + fields: fields.map(([field, type]) => ({ name: field, type })), + methods: [], + loc, + }); + return { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + threadSlotKeys: ["Node"], + globals: [ + ["slots", slotsType], + ["node", node], + ["leaf", leaf], + ["plain", plain], + ["state", state], + ].map(([name, type]) => ({ id: `%g.${name as string}`, name, type, mutable: false })), + functions: [ + { + name: "main", + params: [], + returnType: VOID, + locals: [], + body: body.map((expr) => ({ kind: "exprStmt" as const, expr, loc })), + loc, + }, + ], + classes: [ + cls("%ThreadSlots", []), + cls("Node", [["name", STRING]]), + cls( + "Leaf", + [ + ["name", STRING], + ["weight", F64], + ], + "Node", + ), + cls("Plain", [["name", STRING]]), + cls("State", [["count", F64]]), + ], + unions: [{ id: "u0", arms: [state, { kind: "undefinedT" }] }], + } as unknown as IrModule; +} + +function body(llvm: string, name: string): string { + const start = llvm.indexOf(`@${name}(`); + return start < 0 ? "" : llvm.slice(start, llvm.indexOf("\n}", start)); +} + +const intrinsic = (name: string, args: IrExpr[], type: IrType): IrExpr => + ({ kind: "intrinsic", name, args, type, loc }) as IrExpr; +const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc }); +const global = (id: string, type: IrType): IrExpr => + ({ kind: "varRef", localId: `%g.${id}`, type, loc }) as IrExpr; +const slots = global("slots", slotsType); +const fresh = (type: IrType & { kind: "object" }): IrExpr => + global(type.className.toLowerCase(), type); + +test("slots.get reads the slot word, the page and the entry inline", () => { + const calls = [ + intrinsic("slots.get", [str("s"), slots, fresh(leaf)], maybeState), + intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL), + intrinsic("slots.set", [str("s"), slots, fresh(leaf), fresh(state)], VOID), + intrinsic("slots.delete", [str("t"), slots, fresh(node)], BOOL), + ]; + expect(validateModule(module(calls))).toEqual([]); + const llvm = emitLlvmModule(module(calls), { pointerBits: 64 }); + // The slot word sits after the header words of every hierarchy member. + expect(llvm).toMatch( + new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`), + ); + expect(llvm).toMatch( + new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`), + ); + expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`)); + // One block per family name. + expect(llvm).toMatch( + /@sc_slots_0 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/, + ); + expect(llvm).toMatch( + /@sc_slots_1 = internal global \{ ptr, i32, i32, ptr, ptr \} zeroinitializer/, + ); + const main = body(llvm, "sc_f_main"); + expect(main).toMatch( + /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, + ); + expect(main).not.toMatch(/call ptr @scr_slots_get/); + expect(main).toMatch( + /call void @scr_slots_set\(ptr @sc_slots_0, ptr %\S+, ptr %\S+, ptr %\S+, ptr @\S+\)/, + ); + expect(main).toMatch(/call zeroext i1 @scr_slots_delete\(ptr @sc_slots_1, ptr %\S+\)/); +}); + +test("freeing a key releases its values when its slot word is set", () => { + const llvm = emitLlvmModule( + module([intrinsic("slots.has", [str("s"), slots, fresh(node)], BOOL)]), + { pointerBits: 64 }, + ); + expect(llvm).toMatch(/icmp ne i32 %tdw, 0\n {2}br i1 %tdw\.set, label %tdw\.forget/); + expect(llvm).toMatch(/call void @scr_slots_forget\(i32 %tdw\)/); +}); + +test("the validator checks ThreadSlots intrinsic operands", () => { + const bad = [ + intrinsic("slots.get", [str("s"), slots], maybeState), + intrinsic("slots.has", [slots, slots, fresh(node)], BOOL), + intrinsic("slots.has", [str("s"), slots, fresh(plain)], BOOL), + intrinsic( + "slots.set", + [str("s"), slots, fresh(node), { kind: "numLit", value: 1, type: F64, loc }], + VOID, + ), + intrinsic("slots.get", [str("s"), slots, fresh(node)], state), + ]; + expect(validateModule(module(bad)).map((e) => e.message)).toEqual([ + "in main: slots.get takes 3 arguments", + "in main: slots.has takes a name literal and a %ThreadSlots value", + "in main: slots.has key object:Plain is not a ThreadSlots key class", + "in main: slots.set value f64 is not a ThreadSlots value type", + "in main: slots.get has a V | undefined union type", + ]); +}); diff --git a/packages/compiler/src/backend/llvm/thread-slots.ts b/packages/compiler/src/backend/llvm/thread-slots.ts new file mode 100644 index 000000000..da182bde2 --- /dev/null +++ b/packages/compiler/src/backend/llvm/thread-slots.ts @@ -0,0 +1,170 @@ +/* ThreadSlots (@scriptc/experimental): the slots.* intrinsics (scr_slots.c, + * frontend/lowering/thread-slots.ts). + * + * Each family name has one `{ ptr, i32, i32, ptr, ptr }` ScrSlots block, + * an internal global that is thread-local where module globals are. A key + * class's hierarchy carries an i32 slot word after its header words + * (classSlotWordIndex). A read is inline: the slot word, then the page + * and the entry, + * + * page = word >> 10 < block.npages ? block.pages[word >> 10] : none + * value = page[word & 1023] + * + * and absent pages are a shared page of NULL entries (scr_slots_none), so + * a key without a value reads NULL, the undefined of `V | undefined`. Set + * and delete call the runtime, which holds one reference to each value and + * releases the value it replaces or removes. */ +import { InternalCompilerError } from "../../errors.js"; +import { isUnitType, typeKey, type IrType } from "../../ir/ir.js"; +import { classSlotWordIndex, classStructSym } from "./classes.js"; +import { llvmCommentText } from "./common.js"; +import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js"; +import { releaseSym } from "./shapes.js"; + +const BLOCK = "{ ptr, i32, i32, ptr, ptr }"; + +/** The slots.* intrinsics. */ +export function emitThreadSlotsIntrinsic( + host: LlvmEmitterContext, + e: ExprOf<"intrinsic">, +): LlValue { + const B = host.B; + const [nameArg, slotsArg, keyArg, valueArg] = e.args; + if (nameArg?.kind !== "strLit" || slotsArg === undefined || keyArg === undefined) + throw new InternalCompilerError(`llvm emitter bug: ${e.name} operands`); + const block = host.threadSlotsBlock(nameArg.value); + // The slots value is read for its effects (a module const's TDZ check). + const slots = host.emitReadReceiver(slotsArg); + const key = host.emitReadReceiver(keyArg); + const value = valueArg !== undefined ? host.emitExpr(valueArg) : null; + if (B.isTerminated()) return { name: "", type: e.type }; + if (keyArg.type.kind !== "object") + throw new InternalCompilerError(`llvm emitter bug: ${e.name} key of kind ${keyArg.type.kind}`); + const className = keyArg.type.className; + const word = B.tmp(); + B.line( + `${word} = getelementptr inbounds %${classStructSym(className)}, ptr ${key.name}, i64 0, i32 ${classSlotWordIndex(host.classMetaOf(className))}`, + ); + switch (e.name) { + case "slots.get": { + const read = readSlot(host, block, word); + if (host.nullableUnions.of(e.type) !== null) + return host.own({ name: host.retainValue(read, e.type), type: e.type }); + return boxedRead(host, read, e.type); + } + case "slots.has": { + const read = readSlot(host, block, word); + const has = B.tmp(); + B.line(`${has} = icmp ne ptr ${read}, null`); + return { name: has, type: e.type }; + } + case "slots.delete": { + host.declare(`declare zeroext i1 @scr_slots_delete(ptr, ptr)`); + const had = B.tmp(); + B.line(`${had} = call zeroext i1 @scr_slots_delete(ptr ${block}, ptr ${word})`); + return { name: had, type: e.type }; + } + case "slots.set": { + if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value"); + // The value moves into the block. + host.moveTemp(value); + host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`); + B.line( + `call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`, + ); + if (e.type.kind === "void") return { name: "", type: e.type }; + return host.own({ name: host.retainValue(slots.name, e.type), type: e.type }); + } + default: + throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`); + } +} + +/** A read as a tagged union box, for `V | undefined` types that the module + * keeps boxed (Map keys and Set elements of that type): the value's arm, or + * the interned undefined instance for NULL. */ +function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValue { + const B = host.B; + const def = type.kind === "union" ? host.unionsById.get(type.unionId) : undefined; + const refTag = def?.arms.findIndex((a) => !isUnitType(a)) ?? -1; + const unitTag = def?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1; + if (def === undefined || refTag < 0 || unitTag < 0 || def.arms.length !== 2) + throw new InternalCompilerError(`llvm emitter bug: slots.get of type ${typeKey(type)}`); + const result = B.slot(); + B.entryAllocas.push(`${result} = alloca ptr`); + const has = B.tmp(); + B.line(`${has} = icmp ne ptr ${read}, null`); + const some = B.newLabel("slots.some"), + none = B.newLabel("slots.none"), + done = B.newLabel("slots.box"); + B.condBr(has, some, none); + B.startBlock(some); + const arm = def.arms[refTag]!; + const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm }); + B.line(`store ptr ${box}, ptr ${result}`); + B.br(done); + B.startBlock(none); + B.line(`store ptr ${host.unitInstanceRef(def.id, unitTag)}, ptr ${result}`); + B.br(done); + B.startBlock(done); + const out = B.tmp(); + B.line(`${out} = load ptr, ptr ${result}`); + return host.own({ name: out, type }); +} + +/** This thread's value for the slot word at `word`, borrowed, or NULL. */ +function readSlot(host: LlvmEmitterContext, block: string, word: string): string { + const B = host.B; + host.declare(`@scr_slots_none = external constant [1024 x ptr]`); + const index = B.tmp(), + page = B.tmp(), + pages = B.tmp(), + count = B.tmp(), + inRange = B.tmp(); + B.line(`${index} = load atomic i32, ptr ${word} monotonic, align 4`); + B.line(`${page} = lshr i32 ${index}, 10`); + B.line(`${count} = getelementptr inbounds ${BLOCK}, ptr ${block}, i64 0, i32 1`); + const npages = B.tmp(); + B.line(`${npages} = load i32, ptr ${count}`); + B.line(`${inRange} = icmp ult i32 ${page}, ${npages}`); + const result = B.slot(); + B.entryAllocas.push(`${result} = alloca ptr`); + B.line(`store ptr null, ptr ${result}`); + const found = B.newLabel("slots.page"), + done = B.newLabel("slots.j"); + B.condBr(inRange, found, done); + B.startBlock(found); + B.line(`${pages} = load ptr, ptr ${block}`); + const pageIndex = B.tmp(), + pagePtr = B.tmp(), + entries = B.tmp(), + entryIndex = B.tmp(), + entryWide = B.tmp(), + entry = B.tmp(), + v = B.tmp(); + B.line(`${pageIndex} = zext i32 ${page} to i64`); + B.line(`${pagePtr} = getelementptr inbounds ptr, ptr ${pages}, i64 ${pageIndex}`); + B.line(`${entries} = load ptr, ptr ${pagePtr}`); + B.line(`${entryIndex} = and i32 ${index}, 1023`); + B.line(`${entryWide} = zext i32 ${entryIndex} to i64`); + B.line(`${entry} = getelementptr inbounds ptr, ptr ${entries}, i64 ${entryWide}`); + B.line(`${v} = load ptr, ptr ${entry}`); + B.line(`store ptr ${v}, ptr ${result}`); + B.br(done); + B.startBlock(done); + const out = B.tmp(); + B.line(`${out} = load ptr, ptr ${result}`); + return out; +} + +/** The ScrSlots block definitions of the families a module uses. */ +export function threadSlotsBlockDefinitions( + blocks: ReadonlyMap, + threadLocal: boolean, +): string[] { + const tl = threadLocal ? "thread_local " : ""; + return [...blocks].map( + ([name, sym]) => + `${sym} = internal ${tl}global ${BLOCK} zeroinitializer ; ThreadSlots ${llvmCommentText(JSON.stringify(name))}`, + ); +} diff --git a/packages/compiler/src/backend/native/runtime-inputs.ts b/packages/compiler/src/backend/native/runtime-inputs.ts index 88f049b7c..bc9643670 100644 --- a/packages/compiler/src/backend/native/runtime-inputs.ts +++ b/packages/compiler/src/backend/native/runtime-inputs.ts @@ -70,6 +70,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ] as const; diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 3b15bfeb9..86aa10933 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -1,14 +1,15 @@ /* Provenance of the @scriptc/experimental exports. The package is scriptc's * own (packages/experimental): Node.js runs its JavaScript implementation, * scriptc builds lower its exports (lowering/lifetime-regions.ts, - * lowering/intern-tables.ts) and never compile its body. */ + * lowering/intern-tables.ts, lowering/thread-slots.ts) and never compile + * its body. */ import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import * as ts from "./ts7/adapter.js"; export const EXPERIMENTAL_MODULE = "@scriptc/experimental"; -export type ExperimentalMember = "LifetimeRegion" | "InternTable"; +export type ExperimentalMember = "LifetimeRegion" | "InternTable" | "ThreadSlots"; /** The @scriptc/experimental export an identifier is a named-import binding of. */ export function experimentalImportOf( @@ -27,7 +28,9 @@ export function experimentalImportOf( ) return null; const member = decl.propertyName?.text ?? decl.name.text; - return member === "LifetimeRegion" || member === "InternTable" ? member : null; + return member === "LifetimeRegion" || member === "InternTable" || member === "ThreadSlots" + ? member + : null; } /** Whether `sf` is the package's own declaration file: its directory's @@ -114,3 +117,27 @@ export function internTableMemberOf( ? name : null; } + +export type ThreadSlotsMember = "get" | "has" | "set" | "delete"; + +/** The ThreadSlots member a property access names (`slots.get`), by the + * declaration it resolves to. */ +export function threadSlotsMemberOf( + program: ts.Program, + access: ts.PropertyAccessExpression, + known: Map, +): ThreadSlotsMember | null { + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(access.name); + const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; + const owner = decl.parent; + if ( + !ts.isClassDeclaration(owner) || + owner.name?.text !== "ThreadSlots" || + !isExperimentalDeclarationFile(owner.getSourceFile(), known) + ) + return null; + const name = access.name.text; + return name === "get" || name === "has" || name === "set" || name === "delete" ? name : null; +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 8856ae9b5..e3f82048e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -504,6 +504,9 @@ export interface ClassInfo { /** The synthetic InternTable class (@scriptc/experimental, * intern-tables.ts): synthesized like lifetimeRegion. */ internTable?: true; + /** The synthetic ThreadSlots class (@scriptc/experimental, + * thread-slots.ts): synthesized like lifetimeRegion. */ + threadSlots?: true; /** This class's own `emit` override in the FORWARDING SHAPE (the one * EventEmitter member a subclass may re-declare): never in `methods` — * emit calls keep routing through the emitter spoke, which lowers the diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 391dd7c88..d51d34a6e 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -16,6 +16,7 @@ import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; import { threadsImportOf } from "../threads-import.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; import { lowerInternTableProperty } from "./intern-tables.js"; +import { lowerThreadSlotsProperty } from "./thread-slots.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import { SYMBOL_T } from "../../ir/ir.js"; @@ -1142,6 +1143,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { expr, 'open tables with new InternTable("name")', ); + if (experimentalImportOf(lowerer.program, expr) === "ThreadSlots") + lowerer.noLowering( + "ThreadSlots (@scriptc/experimental) as a value", + expr, + 'open slots with new ThreadSlots("name")', + ); } { const bi = lowerer.builtinImportOf(expr); @@ -1759,6 +1766,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const table = lowerInternTableProperty(lowerer, expr); if (table) return table; } + lowerThreadSlotsProperty(lowerer, expr); // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. const staticField = lowerStaticFieldRead(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 12deec7ad..01328d526 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4,6 +4,7 @@ import { lowerLifetimeRegionNew, } from "./lifetime-regions.js"; import { internTableType, lowerInternTableCall, lowerInternTableNew } from "./intern-tables.js"; +import { lowerThreadSlotsCall, lowerThreadSlotsNew, threadSlotsType } from "./thread-slots.js"; import { isExperimentalDeclarationFile } from "../experimental-import.js"; import { checkCycleBindingAccesses, @@ -2488,6 +2489,15 @@ export class Lowerer { readonly internTableHelpers = new Map(); /** Each InternTable name's type arguments and first construction site. */ readonly internTableNames = new Map(); + /** Each ThreadSlots name's type arguments (thread-slots.ts). */ + readonly threadSlotNames = new Map(); + /** The family each `new ThreadSlots(...)` opens. */ + readonly threadSlotFamilies = new Map< + ts.NewExpression, + { name: string; key: IrType; value: IrType } + >(); + /** Classes used as ThreadSlots keys: their hierarchies carry a slot word. */ + readonly threadSlotKeys = new Set(); readonly forkTargetIdByPath = new Map(); /** Namespace ModuleBlocks this program lowers, filled by splitFiles: * "flattened" — an instantiated namespace whose body joined the file's @@ -2849,6 +2859,10 @@ export class Lowerer { isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) ? internTableType(this) : null, + threadSlotsClass: (decl) => + isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + ? threadSlotsType(this) + : null, moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type), }; // Modules reachable only through literal import() joined @@ -6302,6 +6316,9 @@ export class Lowerer { entry: ENTRY_NAME, ...(this.usesWorkers ? { workers: true } : {}), ...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}), + ...(this.threadSlotKeys.size > 0 + ? { threadSlotKeys: [...this.threadSlotKeys].sort() } + : {}), ...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }), ...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}), }; @@ -6500,7 +6517,7 @@ export class Lowerer { } for (const info of this.classes.values()) { if (info.builtinError) continue; // runtime-provided; nothing lowers - if (info.lifetimeRegion || info.internTable) continue; // synthesized with the class + if (info.lifetimeRegion || info.internTable || info.threadSlots) continue; // synthesized with the class // Generic-class INSTANTIATIONS (and mixin instantiations) are // demand-driven, not units: their members lower unconditionally in // the instance drain below — the generic-fn instance rule. @@ -10916,6 +10933,7 @@ export class Lowerer { return ( lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? + lowerThreadSlotsCall(this, expr) ?? lowerFfiCall(this, expr) ?? lowerFetchCall(this, expr) ?? lowerStaticFetchCompanionCall(this, expr) ?? @@ -11141,6 +11159,7 @@ export class Lowerer { const builtin = lowerLifetimeRegionNew(this, expr) ?? lowerInternTableNew(this, expr) ?? + lowerThreadSlotsNew(this, expr) ?? lowerAbortControllerNew(this, expr) ?? lowerDisposableStackNew(this, expr) ?? lowerEventTargetNew(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts new file mode 100644 index 000000000..9247703f5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -0,0 +1,254 @@ +/* ThreadSlots (@scriptc/experimental): named families of per-thread values + * keyed by class instances (scr_slots.c, backend/llvm/thread-slots.ts). + * + * A ThreadSlots value is an instance of the synthetic class `%ThreadSlots` + * with no fields, so it has the identity, typeof and inspection of the + * Node.js class (whose state is private). A family's per-thread storage is + * found by its name at compile time, so its methods are called through a + * module-level const initialized with `new ThreadSlots("name")`, and + * lower at their call sites to slots.get, slots.has, slots.set and + * slots.delete, with the family name, the const's value and the key: + * - the name must be a string literal, and every construction of one name + * must use the same type arguments; + * - the key type must be a class (its hierarchy carries the slot word); + * - the value type must be a class, an object type, an array, a Map or a + * Set, so `V | undefined` is a nullable reference. + * Any other use of the class or its members as values is refused. */ +import { STRING, VOID, typeKey } from "../../ir/ir.js"; +import type { IrExpr, IrType, SrcLoc } from "../../ir/ir.js"; +import { + type ThreadSlotsMember, + experimentalImportOf, + threadSlotsMemberOf, +} from "../experimental-import.js"; +import * as ts from "../ts7/adapter.js"; +import type { ClassInfo } from "./lower-classes.js"; +import type { Lowerer } from "./lowerer.js"; +import { locOf } from "../program.js"; + +export const THREAD_SLOTS_CLASS = "%ThreadSlots"; + +const BUILTIN_LOC: SrcLoc = { file: "", start: 0, end: 0 }; + +/** Value types a family stores: references that `V | undefined` holds as a + * nullable pointer. */ +const VALUE_KINDS = new Set(["object", "record", "array", "map", "set"]); + +/** The ThreadSlots instance type, registering the class (and its + * constructor) on first use. */ +export function threadSlotsType(lowerer: Lowerer): IrType { + const type: IrType = { kind: "object", className: THREAD_SLOTS_CLASS }; + if (lowerer.classes.has(THREAD_SLOTS_CLASS)) return type; + const info: ClassInfo = { + def: { name: THREAD_SLOTS_CLASS, fields: [], loc: BUILTIN_LOC }, + fields: new Map(), + fieldOrder: [], + methods: new Map(), + decl: null, + threadSlots: true, + ctor: null, + ctorParams: [], + base: null, + subclasses: [], + throwingSetters: [], + staticFields: [], + }; + lowerer.classes.set(THREAD_SLOTS_CLASS, info); + lowerer.liftedFns.push({ + name: `%${THREAD_SLOTS_CLASS}.constructor`, + params: [{ localId: "this.0", name: "this", type }], + returnType: VOID, + locals: [{ id: "this.0", name: "this", type, mutable: false }], + body: [], + loc: BUILTIN_LOC, + }); + return type; +} + +interface Family { + name: string; + key: IrType; + value: IrType; +} + +/** The root of a class's hierarchy. */ +function rootOf(info: ClassInfo): ClassInfo { + let root = info; + while (root.base !== null) root = root.base; + return root; +} + +/** The family a `new ThreadSlots("name")` expression opens, + * validated and registered once per expression. */ +function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family { + const known = lowerer.threadSlotFamilies.get(expr); + if (known !== undefined) return known; + const args = expr.arguments ?? []; + const arg = args[0]; + if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) + lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr); + if (!ts.isStringLiteral(arg!) && !ts.isNoSubstitutionTemplateLiteral(arg!)) + lowerer.unsupported( + "SC1090", + arg!, + "ThreadSlots names other than string literals", + "name the slots with a string literal: every construction of the name opens the same slots", + ); + const name = (arg as ts.StringLiteral).text; + const instance = lowerer.typeOf(expr); + const typeArgs = lowerer.checker.getTypeArguments(instance as ts.TypeReference); + if (typeArgs.length !== 2) lowerer.noLowering("ThreadSlots without type arguments", expr); + const key = lowerer.mapTypeOf(typeArgs[0]!); + const keyInfo = key?.kind === "object" ? lowerer.classes.get(key.className) : undefined; + if ( + key?.kind !== "object" || + keyInfo === undefined || + keyInfo.decl === null || + rootOf(keyInfo).def.runtime === true + ) + lowerer.unsupported( + "SC1090", + expr, + `'${lowerer.checker.typeToString(typeArgs[0]!)}' as a ThreadSlots key type`, + "key the slots by instances of a class declared in the program", + ); + const value = lowerer.mapTypeOf(typeArgs[1]!); + if (value === null || !VALUE_KINDS.has(value.kind)) + lowerer.unsupported( + "SC1090", + expr, + `'${lowerer.checker.typeToString(typeArgs[1]!)}' as a ThreadSlots value type`, + "store class instances, objects, arrays, Maps or Sets", + ); + const signature = `${typeKey(key!)}|${typeKey(value!)}`; + const first = lowerer.threadSlotNames.get(name); + if (first === undefined) lowerer.threadSlotNames.set(name, signature); + else if (first !== signature) + lowerer.unsupported( + "SC1090", + expr, + `opening ThreadSlots ${JSON.stringify(name)} with different type arguments than its first construction`, + "every construction of one slots name must use the same key and value types", + ); + lowerer.threadSlotKeys.add((key as IrType & { kind: "object" }).className); + const family = { name, key: key!, value: value! }; + lowerer.threadSlotFamilies.set(expr, family); + return family; +} + +/** `new ThreadSlots("name")`. */ +export function lowerThreadSlotsNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { + if (!ts.isIdentifier(expr.expression)) return null; + if (experimentalImportOf(lowerer.program, expr.expression) !== "ThreadSlots") return null; + familyOf(lowerer, expr); + return { + kind: "new", + className: THREAD_SLOTS_CLASS, + args: [], + type: threadSlotsType(lowerer), + loc: locOf(expr), + }; +} + +/** The `new ThreadSlots(...)` initializer of the module-level const that + * `receiver` names (through imports and namespace imports), or of the + * slots whose `set` call + * `receiver` is (`slots.set(a, x).set(b, y)`), or null. */ +function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpression | null { + if ( + ts.isCallExpression(receiver) && + ts.isPropertyAccessExpression(receiver.expression) && + threadSlotsMemberOf(lowerer.program, receiver.expression, lowerer.experimentalFiles) === "set" + ) + return declaringNew(lowerer, receiver.expression.expression); + // `slots`, or `ns.slots` through a namespace import. + const ref = ts.isPropertyAccessExpression(receiver) ? receiver.name : receiver; + if (!ts.isIdentifier(ref)) return null; + const checker = lowerer.checker; + let symbol = checker.getSymbolAtLocation(ref); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = checker.getAliasedSymbol(symbol); + const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; + const list = decl.parent; + if ( + !ts.isVariableDeclarationList(list) || + (list.flags & ts.NodeFlags.Const) === 0 || + !ts.isVariableStatement(list.parent) || + !ts.isSourceFile(list.parent.parent) + ) + return null; + const init = decl.initializer; + if (init === undefined || !ts.isNewExpression(init) || !ts.isIdentifier(init.expression)) + return null; + return experimentalImportOf(lowerer.program, init.expression) === "ThreadSlots" ? init : null; +} + +/** The refusal of ThreadSlots members used as values. */ +export function lowerThreadSlotsProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + const member = threadSlotsMemberOf(lowerer.program, expr, lowerer.experimentalFiles); + if (member === null) return null; + return lowerer.noLowering( + `ThreadSlots.${member} (@scriptc/experimental) as a value`, + expr, + `call slots.${member}(...) directly`, + ); +} + +/** `slots.get(k)`, `has`, `set` and `delete`. */ +export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return null; + const member: ThreadSlotsMember | null = threadSlotsMemberOf( + lowerer.program, + callee, + lowerer.experimentalFiles, + ); + if (member === null) return null; + if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined) + lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`); + const arity = member === "set" ? 2 : 1; + if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) + lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr); + const init = declaringNew(lowerer, callee.expression); + if (init === null) + lowerer.unsupported( + "SC1090", + callee.expression, + "ThreadSlots calls through a value other than a module-level const", + 'call the methods through `const slots = new ThreadSlots("name")` declared at module scope', + ); + const family = familyOf(lowerer, init!); + const loc = locOf(expr); + const slotsType = threadSlotsType(lowerer); + const receiver = lowerer.coerceInto( + callee.expression, + lowerer.lowerExpr(callee.expression), + slotsType, + ); + const keyNode = expr.arguments[0]!; + const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key); + const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc }; + const args = [name, receiver, key]; + switch (member) { + case "get": { + const resultTs = lowerer.typeOf(expr); + const result = lowerer.mapTypeOf(resultTs); + if (result === null) lowerer.badType(expr, resultTs); + return { kind: "intrinsic", name: "slots.get", args, type: result!, loc }; + } + case "has": + case "delete": + return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc }; + case "set": { + const valueNode = expr.arguments[1]!; + const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value); + // `set` returns the slots; a statement discards them. + const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType; + return { kind: "intrinsic", name: "slots.set", args: [...args, value], type, loc }; + } + } +} diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 52964e1bd..eab7fd56d 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1075,6 +1075,9 @@ export interface TypeMapperCtx { /** The InternTable instance type (@scriptc/experimental), when `decl` is * that package's class declaration (lowering/intern-tables.ts). */ internTableClass?: (decl: ts.ClassDeclaration) => IrType | null; + /** The ThreadSlots instance type (@scriptc/experimental), when `decl` is + * that package's class declaration (lowering/thread-slots.ts). */ + threadSlotsClass?: (decl: ts.ClassDeclaration) => IrType | null; } /** A generator type's normalized value channels (Generator and the IteratorResult alias share this): @@ -1653,16 +1656,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return hasStr ? STRING : F64; } } - // @scriptc/experimental's LifetimeRegion and InternTable: synthetic - // program classes, provenance-checked by the package's declaration file. + // @scriptc/experimental's LifetimeRegion, InternTable and ThreadSlots: + // synthetic program classes, provenance-checked by the package's + // declaration file. { const classSym = widened.getSymbol(); const name = classSym?.name; - if (name === "LifetimeRegion" || name === "InternTable") { + if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { for (const d of checker.declarationsOf(classSym!)) { if (!ts.isClassDeclaration(d)) continue; const synthetic = - name === "LifetimeRegion" ? ctx.lifetimeRegionClass?.(d) : ctx.internTableClass?.(d); + name === "LifetimeRegion" + ? ctx.lifetimeRegionClass?.(d) + : name === "InternTable" + ? ctx.internTableClass?.(d) + : ctx.threadSlotsClass?.(d); if (synthetic) return synthetic; } } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 0b1f9fb49..ab4b05c8b 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -209,6 +209,7 @@ const MODULE_FIELDS: Record>, Role> = { "contracts.acyclic[]": "other", "contracts.immortalGlobals[]": "other", "contracts.programLifetime[]": "other", + "threadSlotKeys[]": "other", }; const loc = { file: "references.ts", start: 0, end: 0 }; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4041cd265..0f3d0811a 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -955,6 +955,10 @@ export interface IrModule { * ProgramLifetime instance lives in a counted region (the region.* * intrinsics; backend/llvm/regions.ts). */ lifetimeRegions?: true; + /** Classes used as ThreadSlots keys (@scriptc/experimental): the root of + * each one's hierarchy carries a slot word (the slots.* intrinsics; + * backend/llvm/thread-slots.ts). */ + threadSlotKeys?: string[]; /** The entry module is CommonJS under Node's classification. Node runs a * CommonJS main synchronously and drains process.nextTick callbacks * before promise jobs at the first checkpoint; an ES-module main's @@ -3563,7 +3567,19 @@ export type IrExpr = | "itable.find" | "itable.insert" | "itable.value" - | "itable.size"; + | "itable.size" + /* ThreadSlots (@scriptc/experimental; scr_slots.c). Every one takes + * the family name (a string literal), the family's %ThreadSlots + * value (read for its effects only) and the key, a class instance + * whose hierarchy is in IrModule.threadSlotKeys: + * slots.get → V | undefined, a nullable union, owned (+1); + * slots.has → bool; slots.delete → bool, releasing the value; + * slots.set (…, V value) → void, or the %ThreadSlots value when the + * result is used. Nothing throws. */ + | "slots.get" + | "slots.has" + | "slots.set" + | "slots.delete"; args: IrExpr[]; type: IrType; loc: SrcLoc; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index dec6ab201..9a6294f5a 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -985,6 +985,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { errors, mod.workers === true, mod.lifetimeRegions === true, + new Set(mod.threadSlotKeys ?? []), ); } return errors; @@ -1003,6 +1004,7 @@ function validateFunction( errors: IrValidationError[], workers: boolean, lifetimeRegions: boolean, + threadSlotKeys: ReadonlySet, ): void { const locals = new Map(fn.locals.map((l) => [l.id, l])); const err = (message: string, loc: SrcLoc) => @@ -3870,6 +3872,44 @@ function validateFunction( } else if (e.type.kind !== "f64") err(`${e.name} has type f64`, e.loc); return; } + if ( + e.name === "slots.get" || + e.name === "slots.has" || + e.name === "slots.set" || + e.name === "slots.delete" + ) { + for (const a of e.args) checkExpr(a); + const [name, slots, key, value] = e.args; + const arity = e.name === "slots.set" ? 4 : 3; + const isSlots = (t: IrType): boolean => t.kind === "object" && t.className === "%ThreadSlots"; + // The key's class or one of its bases is a declared key. + const keyed = (className: string): boolean => { + for (let c: string | undefined = className; c !== undefined; c = classes.get(c)?.base) + if (threadSlotKeys.has(c)) return true; + return false; + }; + if (e.args.length !== arity) err(`${e.name} takes ${arity} arguments`, e.loc); + else if (name!.kind !== "strLit" || !isSlots(slots!.type)) + err(`${e.name} takes a name literal and a %ThreadSlots value`, e.loc); + else if (key!.type.kind !== "object" || !keyed(key!.type.className)) + err(`${e.name} key ${typeKey(key!.type)} is not a ThreadSlots key class`, e.loc); + else if (e.name === "slots.set") { + const kind = value!.type.kind; + if ( + kind !== "object" && + kind !== "record" && + kind !== "array" && + kind !== "map" && + kind !== "set" + ) + err(`slots.set value ${typeKey(value!.type)} is not a ThreadSlots value type`, e.loc); + if (e.type.kind !== "void" && !isSlots(e.type)) + err("slots.set has type void or %ThreadSlots", e.loc); + } else if (e.name === "slots.get") { + if (e.type.kind !== "union") err("slots.get has a V | undefined union type", e.loc); + } else if (e.type.kind !== "bool") err(`${e.name} has type bool`, e.loc); + return; + } if (e.name === "threads.publish") { if (e.args.length !== 1) err("threads.publish takes exactly one argument", e.loc); for (const a of e.args) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json new file mode 100644 index 000000000..574f51b5d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5727-thread-slots-basics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5727-thread-slots-basics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json new file mode 100644 index 000000000..1fb5e7a76 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5728-thread-slots-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5728-thread-slots-workers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json new file mode 100644 index 000000000..186761e38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5729-thread-slots-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5729-thread-slots-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json new file mode 100644 index 000000000..f278d25e8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/thread-slots-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/thread-slots-fences.ts" + ], + "diags": [] +} diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 413deeff2..8f52f9a5b 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -70,6 +70,49 @@ Programs that behave identically under scriptc and Node.js follow the `@scriptc/ 2. Identity and ids are not compared across threads. Ids assigned inside callbacks are unique in both runtimes but differ between them. 3. A thread does not depend on another thread's inserts. `size` and `has` reflect every thread's inserts in scriptc and the calling thread's under Node.js. -Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a module-level `Map` keyed by the value, or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. +Per-thread state about a shared value (lazily computed results, caches) belongs in thread-local state, for example a `ThreadSlots` family (below), a module-level `Map` keyed by the value, or an array indexed by an id the value carries: module state is per thread in both runtimes. In scriptc an array indexed by a dense id is the fastest of these. In scriptc builds stored values are immortal: they stay allocated until the process exits, including values a callback computed for a key that another thread stored first. + +## ThreadSlots + +A `ThreadSlots` family holds one value per key and thread, like a `WeakMap` that each thread has its own copy of. A thread sees only the values it set, including for keys that every thread shares, such as values read from an `InternTable`, and setting a value never writes the key. + +```ts +import { InternTable, ThreadSlots } from "@scriptc/experimental"; + +class Type { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +class TypeState { + resolved = false; +} + +const types = new InternTable("checker.types"); +// Module scope: every thread that evaluates this line opens the same family. +const states = new ThreadSlots("checker.typeStates"); + +function stateOf(t: Type): TypeState { + let state = states.get(t); + if (state === undefined) { + state = new TypeState(); + states.set(t, state); + } + return state; +} +``` + +- `new ThreadSlots(name)` opens the family called `name`. Within one thread, every construction with the same name refers to the same family. +- `get(key)` and `has(key)` read the calling thread's value for `key`. +- `set(key, value)` sets the calling thread's value and returns the slots. +- `delete(key)` removes the calling thread's value and returns whether there was one. + +scriptc requires the name to be a string literal, every construction of one name to use the same type arguments, the key type to be a class declared in the program (keys may be instances of its subclasses), and the value type to be a class, an object type, an array, a `Map` or a `Set`. The methods must be called through a module-level `const` initialized with `new ThreadSlots("name")`, since a family's storage is found by its name when the program is compiled. + +In scriptc builds every instance of a key class's hierarchy carries a 4-byte slot index, assigned by the first `set` for it and never reused. `get` reads the index, a page of the thread's family and the entry, with no lock, hash or atomic update. A family holds a reference to each of the thread's values and releases it when it is replaced or deleted, when its key is freed, and when the thread exits. Keys freed with a `LifetimeRegion` keep their values until the thread exits, a value that belongs to a region keeps the region allocated while the family holds it, and a value that refers to its own key keeps the key alive. + +Under Node.js each family is a `WeakMap` per thread. diff --git a/packages/experimental/index.d.ts b/packages/experimental/index.d.ts index d831cbff9..77377e25f 100644 --- a/packages/experimental/index.d.ts +++ b/packages/experimental/index.d.ts @@ -52,3 +52,24 @@ export declare class InternTable< * call it. */ getOrInsertComputed(key: K, callback: (key: K) => V): V; } + +/** A named family of per-thread values keyed by objects, like a WeakMap + * that each thread has its own copy of. Every construction with one name + * opens the same family. A value set on one thread is visible only to that + * thread, including for keys that every thread shares (published objects, + * @scriptc/threads), and setting one never writes the key. scriptc requires + * the name to be a string literal, every construction of one name to use + * the same type arguments, the key type to be a class, and the slots to be + * called through a module-level const initialized with `new ThreadSlots`. */ +export declare class ThreadSlots { + /** Opens the family called `name`. */ + constructor(name: string); + /** This thread's value for `key`, or undefined. */ + get(key: K): V | undefined; + /** Whether this thread has a value for `key`. */ + has(key: K): boolean; + /** Sets this thread's value for `key`. Returns the slots. */ + set(key: K, value: V): this; + /** Removes this thread's value for `key`. Returns whether there was one. */ + delete(key: K): boolean; +} diff --git a/packages/experimental/index.js b/packages/experimental/index.js index 8a1b28c26..e5b824ed0 100644 --- a/packages/experimental/index.js +++ b/packages/experimental/index.js @@ -6,6 +6,8 @@ // program no longer references. An InternTable is a Map per name and // thread: Node.js threads share no objects, so every thread has its own // tables, and stored values are published (deep-frozen) as in scriptc. +// ThreadSlots is a WeakMap per name and thread: module state is per thread +// under Node.js, so each thread sees only the values it set. import { publish } from "@scriptc/threads"; @@ -90,3 +92,39 @@ export class InternTable { return value; } } + +/** This thread's slot families, by name. */ +const families = new Map(); + +export class ThreadSlots { + #values; + + /** Opens the family called `name`. */ + constructor(name) { + if (typeof name !== "string") throw new TypeError("The slots name must be a string"); + let values = families.get(name); + if (values === undefined) families.set(name, (values = new WeakMap())); + this.#values = values; + } + + /** This thread's value for `key`, or undefined. */ + get(key) { + return this.#values.get(key); + } + + /** Whether this thread has a value for `key`. */ + has(key) { + return this.#values.has(key); + } + + /** Sets this thread's value for `key`. Returns the slots. */ + set(key, value) { + this.#values.set(key, value); + return this; + } + + /** Removes this thread's value for `key`. Returns whether there was one. */ + delete(key) { + return this.#values.delete(key); + } +} diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 114650325..327f6fc97 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,7 +1,7 @@ { "name": "@scriptc/experimental", "version": "0.3.6", - "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects and intern tables shared between threads", + "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", "repository": { diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..d4607f694 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -45,6 +45,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ]; diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..96add68b6 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -43,6 +43,7 @@ const BASE_RUNTIME_SOURCES = [ "scr_publish.c", "scr_region.c", "scr_itable.c", + "scr_slots.c", "scr_alloc.c", ]; diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index a66d1c554..59cafe208 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -56,6 +56,7 @@ #include "scr_object.h" #include "scr_region.h" #include "scr_itable.h" +#include "scr_slots.h" #include "scr_async.h" #include "scr_events.h" #include "scr_readline.h" diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c new file mode 100644 index 000000000..7c5bc7f88 --- /dev/null +++ b/packages/runtime/src/scr_slots.c @@ -0,0 +1,145 @@ +/* Per-thread slots (ThreadSlots, @scriptc/experimental): see scr_slots.h. */ + +#include "scr_runtime.h" + +#include +#include + +void *const scr_slots_none[SCR_SLOTS_PAGE] = {0}; + +/* The next slot index to assign. 0 means "no index". */ +static _Atomic uint32_t scr_slots_next = 1; + +/* This thread's blocks that hold or held values, and whether their release + * at thread exit is registered. */ +static SCR_TL ScrSlots *scr_slots_blocks; +static SCR_TL bool scr_slots_exit_registered; + +static _Noreturn void scr_slots_oom(void) { scr_trap("scriptc: out of memory\n"); } + +/* A page's entries follow its count of non-NULL entries. */ +static inline uintptr_t *scr_slots_count(void **page) { return (uintptr_t *)page - 1; } + +/* The slot index of the key `obj` with slot word `word`, assigned on first + * use. A published key may be read by every thread, so its index is set + * once with a compare-and-swap and every thread uses the winner's. An + * unpublished key is reachable from this thread only. */ +static uint32_t scr_slots_index(void *obj, uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + if (index != 0) return index; + index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed); + if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n"); + if (*(size_t *)obj != SIZE_MAX) { + __atomic_store_n(word, index, __ATOMIC_RELAXED); + return index; + } + uint32_t expected = 0; + if (__atomic_compare_exchange_n(word, &expected, index, false, __ATOMIC_RELAXED, + __ATOMIC_RELAXED)) + return index; + return expected; +} + +/* Releases the values of every block of this thread and frees their + * pages, leaving the blocks as a later first set finds them. A released + * value can free a key, whose scr_slots_forget then finds an already + * emptied block. */ +static void scr_slots_thread_exit(void) { + if (!scr_exit_frees()) return; + ScrSlots *blocks = scr_slots_blocks; + scr_slots_blocks = NULL; + scr_slots_exit_registered = false; + for (ScrSlots *s = blocks, *next; s != NULL; s = next) { + next = s->next; + void ***pages = s->pages; + uint32_t npages = s->npages; + s->pages = NULL; + s->npages = 0; + s->registered = 0; + s->next = NULL; + for (uint32_t p = 0; p < npages; p++) { + void **page = pages[p]; + if (page == (void **)scr_slots_none) continue; + for (uint32_t i = 0; i < SCR_SLOTS_PAGE; i++) + if (page[i] != NULL) s->release(page[i]); + free(scr_slots_count(page)); + } + free(pages); + } +} + +static void scr_slots_register(ScrSlots *s, void (*release)(void *)) { + s->release = release; + s->registered = 1; + s->next = scr_slots_blocks; + scr_slots_blocks = s; + if (!scr_slots_exit_registered) { + scr_slots_exit_registered = true; + scr_atexit(scr_slots_thread_exit); + } +} + +/* The page holding `index`, created when absent. */ +static void **scr_slots_page(ScrSlots *s, uint32_t index) { + uint32_t p = index >> SCR_SLOTS_PAGE_BITS; + if (p >= s->npages) { + uint32_t n = s->npages ? s->npages : 4; + while (n <= p) n = n > UINT32_MAX / 2 ? (UINT32_MAX >> SCR_SLOTS_PAGE_BITS) + 1 : n * 2; + void ***pages = realloc(s->pages, (size_t)n * sizeof *pages); + if (!pages) scr_slots_oom(); + for (uint32_t i = s->npages; i < n; i++) pages[i] = (void **)scr_slots_none; + s->pages = pages; + s->npages = n; + } + void **page = s->pages[p]; + if (page == (void **)scr_slots_none) { + uintptr_t *block = calloc(SCR_SLOTS_PAGE + 1, sizeof(void *)); + if (!block) scr_slots_oom(); + page = (void **)(block + 1); + s->pages[p] = page; + } + return page; +} + +/* Takes the entry `index` out of `s`; returns its value, or NULL. The + * page goes when its last entry does. */ +static void *scr_slots_take(ScrSlots *s, uint32_t index) { + uint32_t p = index >> SCR_SLOTS_PAGE_BITS; + if (p >= s->npages) return NULL; + void **page = s->pages[p]; + void **entry = &page[index & (SCR_SLOTS_PAGE - 1)]; + void *value = *entry; + if (value == NULL) return NULL; + *entry = NULL; + if (--*scr_slots_count(page) == 0) { + free(scr_slots_count(page)); + s->pages[p] = (void **)scr_slots_none; + } + return value; +} + +void scr_slots_set(ScrSlots *s, void *obj, uint32_t *word, void *value, void (*release)(void *)) { + if (!s->registered) scr_slots_register(s, release); + uint32_t index = scr_slots_index(obj, word); + void **page = scr_slots_page(s, index); + void **entry = &page[index & (SCR_SLOTS_PAGE - 1)]; + void *old = *entry; + *entry = value; + if (old == NULL) ++*scr_slots_count(page); + else s->release(old); +} + +bool scr_slots_delete(ScrSlots *s, const uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + void *old = index ? scr_slots_take(s, index) : NULL; + if (old == NULL) return false; + s->release(old); + return true; +} + +void scr_slots_forget(uint32_t index) { + for (ScrSlots *s = scr_slots_blocks; s != NULL; s = s->next) { + void *old = scr_slots_take(s, index); + if (old != NULL) s->release(old); + } +} diff --git a/packages/runtime/src/scr_slots.h b/packages/runtime/src/scr_slots.h new file mode 100644 index 000000000..5e0307b00 --- /dev/null +++ b/packages/runtime/src/scr_slots.h @@ -0,0 +1,73 @@ +/* scriptc runtime — per-thread slots (scr_slots.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_SLOTS_H +#define SCR_SLOTS_H + +#include +#include +#include + +/* ── per-thread slots (scr_slots.c) ─────────────────────────────────── + * ThreadSlots (@scriptc/experimental): a family of per-thread values keyed + * by class instances. Each instance of a class used as a key carries a + * 32-bit slot word, 0 until a value is first set for it on any thread. + * Setting assigns the word an index that is unique in the process and + * never reused; a published instance, which every thread may read, gets + * its index with a compare-and-swap, so all threads agree on it. + * + * Each family has one ScrSlots block per thread (an emitted thread-local + * global). The block maps indices to that thread's values through pages + * of SCR_SLOTS_PAGE entries, so a read is the slot word, the page and the + * entry, with no hashing, lock or atomic read-modify-write: + * + * page = word >> SCR_SLOTS_PAGE_BITS < npages ? pages[word >> BITS] : none + * value = page[word & (SCR_SLOTS_PAGE - 1)] + * + * Absent pages point at a shared page of NULL entries, and index 0 is + * never assigned, so a word of 0 reads NULL. The emitted read inlines + * this (llvm/thread-slots.ts); scr_slots_get is the same read in C. + * + * A block holds one reference to each value it stores. Values belong to + * the thread: the block releases them when they are replaced or deleted, + * when their key is freed (scr_slots_forget, called by the key class's + * free path), and when the thread exits. */ + +#define SCR_SLOTS_PAGE_BITS 10 +#define SCR_SLOTS_PAGE (1u << SCR_SLOTS_PAGE_BITS) + +/* The emitted layout is `{ ptr, i32, i32, ptr, ptr }`. */ +typedef struct ScrSlots { + void ***pages; /* npages entries; scr_slots_none where unset */ + uint32_t npages; + uint32_t registered; /* on this thread's list of blocks */ + void (*release)(void *); + struct ScrSlots *next; +} ScrSlots; + +/* The page that absent pages point at. */ +extern void *const scr_slots_none[SCR_SLOTS_PAGE]; + +/* This thread's value for the key whose slot word is `word`, borrowed, or + * NULL. */ +static inline void *scr_slots_get(const ScrSlots *s, const uint32_t *word) { + uint32_t index = __atomic_load_n(word, __ATOMIC_RELAXED); + uint32_t page = index >> SCR_SLOTS_PAGE_BITS; + if (page >= s->npages) return NULL; + return s->pages[page][index & (SCR_SLOTS_PAGE - 1)]; +} + +/* Stores `value` (one reference, moved in) for the key `obj`, whose slot + * word is `word`, and releases the value it replaces. `release` releases a + * value of the family's type. */ +void scr_slots_set(ScrSlots *s, void *obj, uint32_t *word, void *value, void (*release)(void *)); + +/* Removes and releases this thread's value for the key; false when there + * was none. */ +bool scr_slots_delete(ScrSlots *s, const uint32_t *word); + +/* The key with slot index `index` is being freed: releases its values in + * every block of the calling thread. Only the thread that owns an + * unpublished key can hold values for it. */ +void scr_slots_forget(uint32_t index); + +#endif /* SCR_SLOTS_H */ diff --git a/packages/runtime/src/scr_slots.test.c b/packages/runtime/src/scr_slots.test.c new file mode 100644 index 000000000..5aba5b266 --- /dev/null +++ b/packages/runtime/src/scr_slots.test.c @@ -0,0 +1,113 @@ +#undef NDEBUG +#include "scr_runtime.h" +#include +#include +#include +#include +#include + +/* The slots unit needs scr_trap, scr_exit_frees and the context's exit + * registration from the rest of the runtime; the exit callback is run + * here at the end of each thread. */ +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + abort(); +} +bool scr_exit_frees(void) { return true; } +static _Thread_local void (*exit_fn)(void); +int scr_context_atexit(void (*fn)(void)) { + assert(exit_fn == NULL || exit_fn == fn); + exit_fn = fn; + return 0; +} + +enum { THREADS = 8, KEYS = 5000 }; + +/* A stand-in for a key object: the count word first, the slot word after. */ +typedef struct { + size_t rc; + uint32_t word; +} Key; + +/* A stand-in for a value: its owner thread and key. */ +typedef struct { + unsigned thread; + unsigned key; +} Value; + +static atomic_size_t released; +static void release(void *v) { + free(v); + atomic_fetch_add(&released, 1); +} + +static Value *value(unsigned thread, unsigned key) { + Value *v = malloc(sizeof *v); + assert(v); + v->thread = thread; + v->key = key; + return v; +} + +static Key shared[KEYS]; +static _Thread_local ScrSlots states; +static _Thread_local ScrSlots others; + +/* Every thread sets its own value for every shared key, in its own order, + * so threads race to assign each key's index, then reads its values back. */ +static void *run(void *arg) { + unsigned t = (unsigned)(uintptr_t)arg; + for (unsigned i = 0; i < KEYS; i++) { + unsigned k = (i * 7919u + t * 104729u) % KEYS; + scr_slots_set(&states, &shared[k], &shared[k].word, value(t, k), release); + } + for (unsigned k = 0; k < KEYS; k++) { + Value *v = scr_slots_get(&states, &shared[k].word); + assert(v && v->thread == t && v->key == k); + assert(scr_slots_get(&others, &shared[k].word) == NULL); + } + /* Replacing releases the old value; deleting releases and reports. */ + scr_slots_set(&states, &shared[0], &shared[0].word, value(t, 0), release); + assert(scr_slots_delete(&states, &shared[1].word)); + assert(!scr_slots_delete(&states, &shared[1].word)); + assert(scr_slots_get(&states, &shared[1].word) == NULL); + + /* Unpublished keys: forget releases a freed key's values in every block + * of the thread. */ + Key *local = calloc(KEYS, sizeof *local); + assert(local); + for (unsigned k = 0; k < KEYS; k++) { + local[k].rc = 1; + scr_slots_set(&states, &local[k], &local[k].word, value(t, k), release); + if (k % 2 == 0) scr_slots_set(&others, &local[k], &local[k].word, value(t, k), release); + } + for (unsigned k = 0; k < KEYS; k += 3) scr_slots_forget(local[k].word); + for (unsigned k = 0; k < KEYS; k++) { + Value *v = scr_slots_get(&states, &local[k].word); + assert(k % 3 == 0 ? v == NULL : v != NULL && v->key == k); + Value *o = scr_slots_get(&others, &local[k].word); + assert(k % 3 == 0 || k % 2 != 0 ? o == NULL : o != NULL && o->key == k); + } + free(local); + + /* Thread exit releases the rest and leaves the blocks empty. */ + assert(exit_fn != NULL); + exit_fn(); + assert(states.npages == 0 && others.npages == 0); + assert(scr_slots_get(&states, &shared[2].word) == NULL); + return NULL; +} + +int main(void) { + for (unsigned k = 0; k < KEYS; k++) shared[k].rc = SIZE_MAX; + pthread_t threads[THREADS]; + for (unsigned t = 0; t < THREADS; t++) + assert(pthread_create(&threads[t], NULL, run, (void *)(uintptr_t)t) == 0); + for (unsigned t = 0; t < THREADS; t++) assert(pthread_join(threads[t], NULL) == 0); + /* Every key got one index, and every value set was released. */ + for (unsigned k = 0; k < KEYS; k++) assert(shared[k].word != 0); + size_t sets = THREADS * ((size_t)KEYS + 1 + KEYS + (KEYS + 1) / 2); + assert(atomic_load(&released) == sets); + printf("thread slots keep one value per thread and key\n"); + return 0; +} diff --git a/packages/runtime/src/scr_slots.test.ts b/packages/runtime/src/scr_slots.test.ts new file mode 100644 index 000000000..80eb8e68b --- /dev/null +++ b/packages/runtime/src/scr_slots.test.ts @@ -0,0 +1,52 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const exec = promisify(execFile); + +/* The slots unit links alone (the test supplies what it calls), so the + * test of racing index assignment runs under ThreadSanitizer as well as + * AddressSanitizer. */ +const lanes = [ + { name: "address", flags: ["-fsanitize=address,undefined"], run: process.platform !== "win32" }, + { name: "thread", flags: ["-fsanitize=thread"], run: process.platform === "linux" }, +]; + +for (const lane of lanes) + test.skipIf(!lane.run)( + `thread slots keep one value per thread and key (${lane.name})`, + async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-slots-")); + try { + const bin = join(dir, "slots"); + await exec("clang", [ + "-std=c11", + "-O1", + "-g", + "-pthread", + "-DSCR_WORKERS", + ...lane.flags, + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + join(import.meta.dirname, "scr_slots.test.c"), + join(import.meta.dirname, "scr_slots.c"), + "-o", + bin, + ]); + const result = await exec(bin, [], { + timeout: 60000, + env: { + ...process.env, + UBSAN_OPTIONS: "halt_on_error=1", + TSAN_OPTIONS: "halt_on_error=1", + }, + }); + expect(result.stdout).toBe("thread slots keep one value per thread and key\n"); + expect(result.stderr).toBe(""); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }, + ); diff --git a/tests/corpus/5727-thread-slots-basics.ts b/tests/corpus/5727-thread-slots-basics.ts new file mode 100644 index 000000000..b8a1c43b5 --- /dev/null +++ b/tests/corpus/5727-thread-slots-basics.ts @@ -0,0 +1,85 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// ThreadSlots (@scriptc/experimental): per-thread values keyed by class +// instances, like a WeakMap. Keys of a base class and its subclasses share +// one family; constructions with one name open the same family. Node.js +// prints the same. + +class Node { + readonly name: string; + constructor(name: string) { + this.name = name; + } +} + +class Leaf extends Node { + readonly weight: number; + constructor(name: string, weight: number) { + super(name); + this.weight = weight; + } +} + +class Info { + count = 0; + readonly label: string; + constructor(label: string) { + this.label = label; + } +} + +const info = new ThreadSlots("basics.info"); +const sameInfo = new ThreadSlots("basics.info"); +const other = new ThreadSlots("basics.other"); + +function infoOf(n: Node): Info { + let found = info.get(n); + if (found === undefined) { + found = new Info(`info of ${n.name}`); + info.set(n, found); + } + found.count++; + return found; +} + +const a = new Node("a"); +const b = new Leaf("b", 2); +console.log(info.get(a), info.has(a), info.delete(a)); +console.log(infoOf(a).label, infoOf(a).count, infoOf(b).label, infoOf(b).count); +console.log(info.has(a), info.has(b), sameInfo.get(a)?.count, other.has(a)); + +// set returns the slots, so calls chain. +const c = new Leaf("c", 3); +const chained = other.set(a, new Info("other a")).set(c, new Info("other c")); +console.log(chained === other, other.get(a)?.label, other.get(c)?.label, other.has(b)); + +// Replacing and deleting. +other.set(a, new Info("other a again")); +console.log(other.get(a)?.label, other.delete(a), other.delete(a), other.get(a)); +console.log(sameInfo.delete(b), info.has(b), info.get(b)); + +// Many keys: values come back for each, after keys around them go away. +const kept: Node[] = []; +for (let i = 0; i < 5000; i++) { + const n = i % 2 === 0 ? new Node(`n${i}`) : new Leaf(`l${i}`, i); + info.set(n, new Info(`#${i}`)); + if (i % 3 === 0) kept.push(n); +} +let sum = 0; +for (const n of kept) sum += Number(info.get(n)!.label.slice(1)); +console.log(kept.length, sum, info.get(kept[kept.length - 1]!)?.label); + +// A key whose value refers back to the key. +class Holder { + readonly node: Node; + constructor(node: Node) { + this.node = node; + } +} +const backs = new ThreadSlots("basics.back"); +for (let i = 0; i < 100; i++) { + const n = new Node(`back${i}`); + backs.set(n, new Holder(n)); + if (i === 99) console.log(backs.get(n)?.node === n, backs.get(n)?.node.name); +} +console.log(typeof info); diff --git a/tests/corpus/5728-thread-slots-workers.ts b/tests/corpus/5728-thread-slots-workers.ts new file mode 100644 index 000000000..70bee484f --- /dev/null +++ b/tests/corpus/5728-thread-slots-workers.ts @@ -0,0 +1,107 @@ +import { Worker, isMainThread, parentPort, threadId, workerData } from "node:worker_threads"; +import { InternTable, ThreadSlots } from "@scriptc/experimental"; +import type { ProgramLifetime } from "@scriptc/types"; + +// ThreadSlots on shared keys: workers intern types in an InternTable, so in +// scriptc builds every thread reads the same published Type objects, and +// each thread keeps its own derived state for them in one ThreadSlots +// family. A thread sees only the values it set. Keys are ProgramLifetime +// instances, and some get a value before they are published. Node.js +// prints the same. + +class Type implements ProgramLifetime { + readonly id: number; + readonly name: string; + readonly members: readonly Type[]; + constructor(id: number, name: string, members: readonly Type[]) { + this.id = id; + this.name = name; + this.members = members; + } +} + +class TypeState { + readonly thread: number; + printed: string; + reads = 0; + constructor(thread: number, printed: string) { + this.thread = thread; + this.printed = printed; + } +} + +let nextLocalId = 0; +function nextId(): number { + return threadId * 1000000 + nextLocalId++; +} + +const interned = new InternTable("slots.types"); +const states = new ThreadSlots("slots.states"); + +function named(name: string): Type { + return interned.getOrInsertComputed(name, () => new Type(nextId(), name, [])); +} + +function listOf(element: Type): Type { + const key = `list:${print(element)}`; + const found = interned.get(key); + if (found !== undefined) return found; + // A value set before the type is published stays with it. + const fresh = new Type(nextId(), "list", [element]); + states.set(fresh, new TypeState(threadId, `${print(element)}[]`)); + return interned.getOrInsert(key, fresh); +} + +function print(t: Type): string { + let state = states.get(t); + if (state === undefined) { + const text = t.name === "list" ? `${print(t.members[0]!)}[]` : t.name; + state = new TypeState(threadId, text); + states.set(t, state); + } + state.reads++; + return state.printed; +} + +function run(seed: number): string { + const base = ["string", "number", "boolean", "bigint"].map(named); + const made: Type[] = [...base]; + let state = seed; + for (let r = 0; r < 2000; r++) { + state = (state * 1103515245 + 12345) % 2147483648; + const t = made[state % made.length]!; + made.push(r % 4 === 0 ? listOf(t) : t); + } + let reads = 0; + let own = 0; + const forms = new Set(); + for (const t of made) { + forms.add(print(t)); + const s = states.get(t)!; + reads += s.reads; + if (s.thread === threadId) own++; + } + let removed = 0; + for (const t of base) if (states.delete(t)) removed++; + return `${made.length} types, ${forms.size} forms, own state ${own === made.length}, reads ${reads > made.length}, removed ${removed}, after ${states.has(base[0]!)}`; +} + +if (isMainThread) { + const lines: string[] = []; + let pending = 3; + for (let w = 0; w < 3; w++) { + const worker = new Worker(new URL(import.meta.url), { workerData: { seed: w * 31 + 7 } }); + worker.on("message", (message: unknown) => { + lines[w] = `worker ${w}: ${message as string}`; + }); + worker.on("exit", () => { + if (--pending === 0) { + for (const line of lines) console.log(line); + console.log(`main: ${run(3)}`); + } + }); + } +} else { + const { seed } = workerData as { seed: number }; + parentPort!.postMessage(run(seed)); +} diff --git a/tests/corpus/5729-thread-slots-values.ts b/tests/corpus/5729-thread-slots-values.ts new file mode 100644 index 000000000..dbe905a06 --- /dev/null +++ b/tests/corpus/5729-thread-slots-values.ts @@ -0,0 +1,89 @@ +import { ThreadSlots } from "@scriptc/experimental"; +import { publish } from "@scriptc/threads"; + +// ThreadSlots value types (class instances, objects, arrays, Maps and +// Sets), families keyed by a base class and by one of its subclasses, and +// published keys. Node.js prints the same. + +class Shape { + readonly sides: number; + constructor(sides: number) { + this.sides = sides; + } +} + +class Square extends Shape { + readonly size: number; + constructor(size: number) { + super(4); + this.size = size; + } +} + +class Point { + readonly x: number; + readonly y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +interface Note { + text: string; + seen: number; +} + +const notes = new ThreadSlots("values.notes"); +const lists = new ThreadSlots("values.lists"); +const maps = new ThreadSlots>("values.maps"); +const sets = new ThreadSlots>("values.sets"); +const points = new ThreadSlots("values.points"); + +const triangle = new Shape(3); +const square = new Square(2); +notes.set(triangle, { text: "three", seen: 0 }); +notes.set(square, { text: "four", seen: 0 }); +notes.get(square)!.seen++; +lists.set(square, ["a"]); +lists.get(square)!.push("b"); +maps.set(square, new Map([["area", 4]])); +maps.get(square)!.set("perimeter", 8); +console.log(notes.get(triangle), notes.get(square), lists.get(square), lists.has(triangle)); +console.log([...maps.get(square)!], maps.has(new Square(2))); + +const origin = new Point(0, 0); +sets.set(origin, new Set([1, 2])); +sets.get(origin)!.add(3); +points.set(origin, new Point(1, 1)); +points.set(points.get(origin)!, origin); +console.log([...sets.get(origin)!], points.get(points.get(origin)!) === origin); + +// Published keys: the slots never write the key. +const frozen = publish(new Square(5)); +notes.set(frozen, { text: "published", seen: 1 }); +maps.set(frozen, new Map()); +console.log(notes.get(frozen)?.text, maps.get(frozen)?.size, frozen.size); +console.log(notes.delete(frozen), notes.has(frozen), maps.has(frozen)); + +// Replaced values and freed keys. +let total = 0; +for (let round = 0; round < 3; round++) { + const shapes: Shape[] = []; + for (let i = 0; i < 2000; i++) { + const s = i % 2 === 0 ? new Shape(i) : new Square(i); + shapes.push(s); + notes.set(s, { text: `first ${i}`, seen: 0 }); + notes.set(s, { text: `second ${i}`, seen: round }); + lists.set(s, [String(i)]); + } + for (const s of shapes) total += notes.get(s)!.seen + lists.get(s)!.length; +} +console.log(total, notes.get(triangle)?.text); + +// `Note | undefined` as Set elements: reads of that type are tagged boxes. +const seen = new Set(); +seen.add(notes.get(triangle)); +seen.add(notes.get(new Shape(9))); +seen.add(notes.get(triangle)); +console.log(seen.size, [...seen].map((n) => n?.text ?? "none").join(",")); diff --git a/tests/diagnostics/thread-slots-fences.ts b/tests/diagnostics/thread-slots-fences.ts new file mode 100644 index 000000000..7ec71db34 --- /dev/null +++ b/tests/diagnostics/thread-slots-fences.ts @@ -0,0 +1,29 @@ +import { ThreadSlots } from "@scriptc/experimental"; + +// ThreadSlots (@scriptc/experimental) forms scriptc refuses: names that are +// not string literals, one name opened with two value types, key and value +// types the slots cannot hold, calls through values other than a +// module-level const, and the class or its methods as values. + +class Key { + readonly id = 0; +} +class State { + count = 0; +} +interface Shape { + readonly id: number; +} + +const name = "dynamic"; +const named = new ThreadSlots(name); +const first = new ThreadSlots("fences.shared"); +const second = new ThreadSlots("fences.shared"); +const shaped = new ThreadSlots("fences.shaped"); +const callbacks = new ThreadSlots number>("fences.callbacks"); +function local(slots: ThreadSlots, key: Key): State | undefined { + return slots.get(key); +} +const ctor = ThreadSlots; +const getter = first.get; +console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); diff --git a/tests/harness/__snapshots__/thread-slots-fences.ts.txt b/tests/harness/__snapshots__/thread-slots-fences.ts.txt new file mode 100644 index 000000000..566ddfba6 --- /dev/null +++ b/tests/harness/__snapshots__/thread-slots-fences.ts.txt @@ -0,0 +1,62 @@ +thread-slots-fences.ts:19:43 - error SC1090: ThreadSlots names other than string literals are not supported yet + + 18 | const name = "dynamic"; + 19 | const named = new ThreadSlots(name); + | ^~~~ + 20 | const first = new ThreadSlots("fences.shared"); + + hint: name the slots with a string literal: every construction of the name opens the same slots + +thread-slots-fences.ts:21:16 - error SC1090: opening ThreadSlots "fences.shared" with different type arguments than its first construction is not supported yet + + 20 | const first = new ThreadSlots("fences.shared"); + 21 | const second = new ThreadSlots("fences.shared"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 | const shaped = new ThreadSlots("fences.shaped"); + + hint: every construction of one slots name must use the same key and value types + +thread-slots-fences.ts:22:16 - error SC1090: 'Shape' as a ThreadSlots key type is not supported yet + + 21 | const second = new ThreadSlots("fences.shared"); + 22 | const shaped = new ThreadSlots("fences.shaped"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 | const callbacks = new ThreadSlots number>("fences.callbacks"); + + hint: key the slots by instances of a class declared in the program + +thread-slots-fences.ts:23:19 - error SC1090: '() => number' as a ThreadSlots value type is not supported yet + + 22 | const shaped = new ThreadSlots("fences.shaped"); + 23 | const callbacks = new ThreadSlots number>("fences.callbacks"); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 24 | function local(slots: ThreadSlots, key: Key): State | undefined { + + hint: store class instances, objects, arrays, Maps or Sets + +thread-slots-fences.ts:25:10 - error SC1090: ThreadSlots calls through a value other than a module-level const is not supported yet + + 24 | function local(slots: ThreadSlots, key: Key): State | undefined { + 25 | return slots.get(key); + | ^~~~~ + 26 | } + + hint: call the methods through `const slots = new ThreadSlots("name")` declared at module scope + +thread-slots-fences.ts:27:14 - error SC2020: 'ThreadSlots (@scriptc/experimental) as a value' is part of the standard library types but has no scriptc lowering yet + + 26 | } + 27 | const ctor = ThreadSlots; + | ^~~~~~~~~~~ + 28 | const getter = first.get; + + hint: open slots with new ThreadSlots("name") + +thread-slots-fences.ts:28:16 - error SC2020: 'ThreadSlots.get (@scriptc/experimental) as a value' is part of the standard library types but has no scriptc lowering yet + + 27 | const ctor = ThreadSlots; + 28 | const getter = first.get; + | ^~~~~~~~~ + 29 | console.log(named, second, shaped, callbacks, local(first, new Key()), ctor, getter); + + hint: call slots.get(...) directly \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 7f89295df..e66295a04 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1841,6 +1841,9 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5727-thread-slots-basics.ts": 3100, + "5728-thread-slots-workers.ts": 2500, + "5729-thread-slots-values.ts": 2550, "5800-broadcast-channel.ts": 2899, "5801-broadcast-channel.cjs": 2727, "5802-broadcast-channel-workers.ts": 2809, @@ -4525,6 +4528,9 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5727-thread-slots-basics.ts": 4500, + "5728-thread-slots-workers.ts": 5000, + "5729-thread-slots-values.ts": 4400, "5800-broadcast-channel.ts": 28374, "5801-broadcast-channel.cjs": 27251, "5802-broadcast-channel-workers.ts": 27251, diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 0cb55f690..5d814b384 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -310,6 +310,9 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { "5721-intern-table-values.ts", // Array key sizes are checked in 32-bit size_t arithmetic. "5726-intern-table-key-limits.ts", + // ThreadSlots blocks and pages hold 32-bit pointers. + "5727-thread-slots-basics.ts", + "5729-thread-slots-values.ts", ])("the LLVM target runs %s byte-identically to Node", async (fixture) => { const entry = join(repoRoot, "tests/corpus", fixture); const outDir = await mkdtemp("/tmp/scriptc-wasi-"); From 56e472a225ebe8888c2e4d802a29d57705d69176 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 11:04:03 -0500 Subject: [PATCH 02/67] Return undefined from generic functions that read a missing element A generic function, arrow or method whose body returns an array element or an index-signature entry now returns undefined for a missing one, as in Node, instead of throwing. Only declarations whose bodies return such a read change their instance result type. Fixes #964 Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com> --- .../src/frontend/lowering/function-returns.ts | 7 ++ .../frontend/lowering/generic-functions.ts | 9 +- .../compiler/src/frontend/lowering/lowerer.ts | 92 +++++++++++++++++++ .../5954-generic-optional-returns.ts.json | 6 ++ tests/corpus/5954-generic-optional-returns.ts | 51 ++++++++++ 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json create mode 100644 tests/corpus/5954-generic-optional-returns.ts diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index d3c5eb1ba..4f519691f 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -157,6 +157,13 @@ export function declaredReturnType( } if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl)) mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn; + // A generic-class method body that can return an indexed read that + // misses returns undefined there, whatever the declared result says. + if (mappedReturn && lowerer.runtimeOptionalGenericReturns.has(decl)) + mappedReturn = + mappedReturn.kind === "promise" + ? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) } + : lowerer.runtimeOptionalType(mappedReturn); if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") { let returnsValue = false; const scan = (node: ts.Node): void => { diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bd785a256..90f83ec2c 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -292,11 +292,16 @@ export function genericCallInstance( (_param, i) => expr.arguments[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); - const returnType = lowerer.mapTypeOf(retTs); - if (!returnType) { + const mappedReturn = lowerer.mapTypeOf(retTs); + if (!mappedReturn) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } + // A body that can return an indexed read that misses returns undefined + // there, as in JavaScript, whatever the declared result says. + const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) + ? lowerer.runtimeOptionalType(mappedReturn) + : mappedReturn; // keyof-constrained type parameters (`K extends keyof T`): the bound // LITERAL is semantic — the instance body reads the named field — so diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index b77768c28..40cf40de9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1880,6 +1880,10 @@ export class Lowerer { * declared optional binding to that read's bare type, so presence tests * on these bindings consult the stored tag instead. */ readonly runtimeOptionalAssignedSymbols = new Set(); + /** Generic function declarations whose body can return an indexed read + * that misses. + * Each instance's result admits undefined (generic-functions.ts). */ + readonly runtimeOptionalGenericReturns = new Set(); /** Concise callback/lambda returns promoted by the HOF callback prepass; * arrows have no declaration symbol to key in fnSigsBySymbol. */ readonly runtimeOptionalFunctionReturns = new Map(); @@ -4289,6 +4293,79 @@ export class Lowerer { const read = this.mapTypeOf(this.typeOf(e)); return !(read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0); }; + /** An optional value in a generic body, where element types are type + * parameters with no IR mapping yet: an element read of an array or a + * read of a string index signature, judged on checker types. */ + const genericClassMethods: ts.MethodDeclaration[] = []; + for (const sf of sourceFiles) { + if (isJsSourceFile(sf) || sf.isDeclarationFile) continue; + ts.walkPreorder(sf, (node) => { + if (!ts.isClassDeclaration(node)) return undefined; + const genericClass = (node.typeParameters?.length ?? 0) > 0; + for (const member of node.members) + if ( + ts.isMethodDeclaration(member) && + member.body && + (genericClass || (member.typeParameters?.length ?? 0) > 0) + ) + genericClassMethods.push(member); + return undefined; + }); + } + const genericMayBeOptional = (node: ts.Expression): boolean => { + if (explicitlyNonNull(node)) return false; + if (mayBeOptional(node)) return true; + const e = peel(node); + if (ts.isConditionalExpression(e)) + return genericMayBeOptional(e.whenTrue) || genericMayBeOptional(e.whenFalse); + if (ts.isBinaryExpression(e)) { + const op = e.operatorToken.kind; + if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) + return genericMayBeOptional(e.right); + if (op === ts.SyntaxKind.AmpersandAmpersandToken) + return genericMayBeOptional(e.left) || genericMayBeOptional(e.right); + return false; + } + if (ts.isIdentifier(e)) { + // A binding initialized from such a read and never reassigned. + const symbol = symbolOf(e); + const decl = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + return ( + symbol !== null && + !reassigned.has(symbol) && + decl !== undefined && + ts.isVariableDeclaration(decl) && + decl.initializer !== undefined && + ts.isIdentifier(decl.name) && + !provenPresent(e, symbol) && + genericIndexedRead(decl.initializer) + ); + } + return genericIndexedRead(e); + }; + const genericIndexedRead = (node: ts.Expression): boolean => { + const e = peel(node); + if (!ts.isElementAccessExpression(e) || explicitlyNonNull(node)) return false; + const read = typeOrNull(e); + if (!read || (read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any)) !== 0) return false; + if ( + read.isUnionType() && + ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0) + ) + return false; + const receiver = typeOrNull(e.expression); + if (!receiver || this.checker.isTupleType(receiver)) return false; + if (this.checker.isArrayType(receiver)) return true; + const key = e.argumentExpression; + if ( + (ts.isStringLiteral(key) || ts.isNumericLiteral(key)) && + this.checker.getPropertyOfType(receiver, key.text) !== undefined + ) + return false; + return this.checker + .getIndexInfosOfType(receiver) + .some((info) => (info.keyType.flags & ts.TypeFlags.StringLike) !== 0); + }; const isDynamicObjectEntryRead = (node: ts.Expression): boolean => { const read = peel(node); if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false; @@ -5815,6 +5892,21 @@ export class Lowerer { for (const sf of sourceFiles) if (scanFile(sf)) changed = true; for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true; + const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => { + if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return; + if (!returnsOf(decl).some(genericMayBeOptional)) return; + optionalReturns.add(symbol); + this.runtimeOptionalGenericReturns.add(decl); + provenance?.note("return", decl.name ?? decl, decl); + changed = true; + }; + for (const [symbol, info] of this.genericFnsBySymbol) noteGenericReturn(symbol, info.decl); + // Generic methods and methods of a generic class collect with their + // class, after this analysis; their result admits undefined there. + for (const method of genericClassMethods) { + const symbol = method.name ? symbolOf(method.name) : null; + if (symbol) noteGenericReturn(symbol, method); + } for (const fn of tsLambdas) { // An annotated return is the author's contract, as for parameters, // except that a var read before its first store is undefined diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json new file mode 100644 index 000000000..a57275c12 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5954-generic-optional-returns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5954-generic-optional-returns.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5954-generic-optional-returns.ts b/tests/corpus/5954-generic-optional-returns.ts new file mode 100644 index 000000000..9aaec5889 --- /dev/null +++ b/tests/corpus/5954-generic-optional-returns.ts @@ -0,0 +1,51 @@ +// A generic function, method or arrow that returns an element read or an +// index-signature read answers undefined for a missing element or key, as +// in Node, for every instantiation. +export {}; + +function at(items: T[], index: number): T { + return items[index]; +} +function lookup(table: Record, key: string): T { + return table[key]; +} +function viaBinding(items: T[], index: number): T { + const item = items[index]; + return item; +} +function either(items: T[], index: number, fallback: T): T { + return index >= 0 ? items[index] : fallback; +} +const first = (items: T[]): T => items[0]; + +console.log(at([1, 2], 1), at([1, 2], 5), at(["a"], 0), at(["a"], 3)); +console.log(lookup({ a: 1 }, "a"), lookup({ a: 1 }, "b"), lookup({ s: "x" }, "t")); +console.log(viaBinding([true], 0), viaBinding([true], 2)); +console.log(either([1], 4, 9), either([1], -1, 9)); +console.log(first([7]), first([]), first(["only"])); + +const missing = at([10, 20], 7); +console.log(missing === undefined, typeof missing); +const label = lookup({ a: "A" }, "z"); +console.log(label ?? "none", label === undefined); + +class Store { + items: Record; + constructor(items: Record) { + this.items = items; + } + get(key: string): T { + return this.items[key]; + } + pick(keys: K[], index: number): K { + return keys[index]; + } +} +const store = new Store({ a: 1 }); +console.log(store.get("a"), store.get("b"), store.pick(["x", "y"], 1), store.pick(["x"], 4)); + +const pairs: [string, number][] = [["a", 1]]; +function second(rows: T[][], row: number, column: number): T { + return rows[row][column]; +} +console.log(second([[1, 2]], 0, 1), second([[1, 2]], 0, 9), second(pairs, 0, 1)); From 19c742834e7eedf2f258fd72ee6820ab3a8d0abc Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 08:20:18 -0700 Subject: [PATCH 03/67] Add an experimental young-in nursery for compiled classes and records SCRIPTC_YOUNG_NURSERY=1 at compile time makes an executable allocate traced, program-owned class instances and records young and link the collector unit built with SCR_YIN (a runtime pack variant of scr_cycle.c; libraries never get it). The emitted constructor lists the object in a per-thread nursery and marks its header: generation SCR_CYC_YOUNG and its nursery index under the buffered bit, so every existing release path treats it as a buffered candidate (no call while it stays alive; scr_cyc_on_dead takes it out of the nursery at its death). A nursery collection (whenever 4096 young objects are live, at collection points, before full sweeps and limited passes) counts each young object's references from other young objects, marks from the objects whose count is below their reference count (referenced from outside the nursery: stacks, globals, older objects, runtime structures, FFI, fibers, coroutine frames), frees what stays unmarked, and gives up the dead set's outgoing edges after the teardowns. The counts are recomputed at every collection instead of maintained by a store barrier, so soundness rests only on the invariants trial deletion already needs. Survivors join trial deletion's nursery and outside roots become candidates; trial deletion walks young objects it reaches as nursery-level objects. Per-type admission statistics send types that do not die in young cycles back to trial deletion for a while. Publication takes young objects out of the nursery. The RC audit build checks nursery membership and counts at every collection. With the flag off, the compiler emits and links exactly what it did before (249 programs and the self-hosted compiler compared byte-identical). Measured with the flag on: cg-rings -38%, cg-pairs -27%, the runtime suite geomean -0.3% (published-graphs +3.4%), the self-hosted compiler neutral; shapes whose cycles go through arrays or closures (phase 2) pay without gaining. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/executable-features.ts | 2 + .../compiler/src/backend/llvm/classes.test.ts | 51 +- packages/compiler/src/backend/llvm/classes.ts | 6 + packages/compiler/src/backend/llvm/emitter.ts | 2 + packages/compiler/src/backend/llvm/shapes.ts | 68 ++- .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 2 + .../compiler/src/backend/native/executable.ts | 2 + .../compiler/src/backend/runtime-pack-core.ts | 2 + .../compiler/src/backend/young-nursery.ts | 9 + .../src/executable/executable-cache.ts | 8 +- packages/compiler/src/index.ts | 3 + packages/compiler/src/native/prepare.ts | 2 + .../compiler/src/native/sanitizer.test.ts | 1 + .../corpus/5981-young-nursery-cycles.ts.json | 6 + .../corpus/5982-young-nursery-workers.ts.json | 6 + .../runtime-pack-matrix.mjs | 9 + .../runtime-darwin-arm64/scripts/build.mjs | 2 +- .../runtime-pack-common/library-matrix.mjs | 6 +- .../runtime-pack-matrix.mjs | 9 + .../runtime-pack-common/scripts/build.mjs | 2 +- .../test/library-matrix.test.ts | 17 + packages/runtime/src/scr_cycle.c | 417 ++++++++++++++++- packages/runtime/src/scr_rc.h | 32 ++ packages/runtime/test/test_yin.c | 442 ++++++++++++++++++ packages/runtime/test/yin.test.ts | 80 ++++ tests/corpus/5981-young-nursery-cycles.ts | 178 +++++++ tests/corpus/5982-young-nursery-workers.ts | 81 ++++ 28 files changed, 1439 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/src/backend/young-nursery.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json create mode 100644 packages/runtime/test/test_yin.c create mode 100644 packages/runtime/test/yin.test.ts create mode 100644 tests/corpus/5981-young-nursery-cycles.ts create mode 100644 tests/corpus/5982-young-nursery-workers.ts diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 553643470..a565965f1 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -3,11 +3,13 @@ import { type IrModule } from "../ir/ir.js"; import { moduleRuntimeFeatures } from "../ir/runtime-features.js"; import { hasForeignFfiCallback } from "./ffi-callbacks.js"; import type { NativeLinkFeatures } from "./native-link-info.js"; +import { youngNurseryRequested } from "./young-nursery.js"; export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { const features = moduleRuntimeFeatures(mod); return { ...(mod.workers ? { workers: true } : {}), + ...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}), regex: features.regex, copying: features.copying, textDecoderLegacy: features.legacyTextDecoder, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 309d8f071..d1dec3f97 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; +import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js"; import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js"; import { emitLlvmModule } from "./emitter.js"; import { @@ -316,3 +316,52 @@ test("unconstructed narrowing layouts may omit abstract slots while live classes }); expect(() => emitLlvmModule(mod)).toThrow("no implementation of run for Dormant"); }); + +test("the young-in nursery flag adds the nursery entry to traced constructors only", () => { + const linked = { kind: "object" as const, className: "Linked" }; + const mod: IrModule = { + irVersion: 16, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { name: "Scalar", fields: [{ name: "value", type: F64 }], methods: [], loc }, + // A dyn field can reach anything: the class carries a collector header. + { + name: "Linked", + fields: [ + { name: "next", type: linked }, + { name: "hook", type: DYN }, + ], + methods: [], + loc, + }, + ], + }; + const helper = (llvm: string, name: string): string => { + const start = llvm.indexOf(`define internal ptr @${name}(`); + expect(start).toBeGreaterThanOrEqual(0); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); + }; + const saved = process.env["SCRIPTC_YOUNG_NURSERY"]; + try { + delete process.env["SCRIPTC_YOUNG_NURSERY"]; + const off = emitLlvmModule(mod, { pointerBits: 64 }); + expect(off).not.toContain("scr_yin"); + + process.env["SCRIPTC_YOUNG_NURSERY"] = "1"; + const on = emitLlvmModule(mod, { pointerBits: 64 }); + const linkedNew = helper(on, mangleClassNew("Linked")); + expect(linkedNew).toContain("@scr_yin_skip"); + expect(linkedNew).toContain("call void @scr_yin_enter_slow(ptr %o)"); + expect(linkedNew).toContain("store i8 3, ptr %ye.genp"); + // An acyclic class has no collector header and never enters. + expect(helper(on, mangleClassNew("Scalar"))).not.toContain("scr_yin"); + // Targets without the inline allocator call the whole entry. + const narrow = emitLlvmModule(mod, { pointerBits: 32 }); + expect(helper(narrow, mangleClassNew("Linked"))).toContain("call void @scr_yin_enter(ptr %o)"); + } finally { + if (saved === undefined) delete process.env["SCRIPTC_YOUNG_NURSERY"]; + else process.env["SCRIPTC_YOUNG_NURSERY"] = saved; + } +}); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 8a13d83ad..456b4b187 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -51,6 +51,7 @@ import { retainBody, traceAdapter, type ShapeHost, + youngEntryLines, } from "./shapes.js"; /** One virtual method slot of a hierarchy: the ROOT-MOST declaring class @@ -803,6 +804,11 @@ export function emitClassShapes( ); } nw.push(...undefFieldInits(host, meta, nullable)); + // Young instances: program-owned traced classes. Classes rooted at a + // runtime base (errors, emitters, events, streams) carry runtime state + // and stay with trial deletion, like every runtime family. + if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true)) + nw.push(...youngEntryLines(host)); nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``); defs.push(...nw); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2c3a71457..6d0973a05 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3,6 +3,7 @@ import { emitStringInputs } from "./string-lifetimes.js"; import { emitLlvmLayouts } from "./layouts.js"; import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js"; import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js"; +import { youngNurseryRequested } from "../young-nursery.js"; import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js"; import { InternalCompilerError } from "../../errors.js"; import { createHash } from "node:crypto"; @@ -1334,6 +1335,7 @@ export class LlEmitter { ...(this.rcMode !== undefined ? { rcMode: this.rcMode } : {}), ...(this.rcHelpersNoinline ? { rcHelpersNoinline: true } : {}), nextCycKind: () => FIRST_EMITTED_CYC_KIND + this.cycKinds++, + ...(youngNurseryRequested() && mod.lib === undefined ? { youngNursery: true } : {}), unionsById: this.unionsById, declare: (decl) => this.declare(decl), needOom: () => this.needOom(), diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 23565eff4..32a0ca006 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -28,7 +28,7 @@ import { mangleRecordStruct, mangleRecordTrace, } from "../mangle.js"; -import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; +import { CYC_HDR, cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js"; import type { BlockBuilder } from "./blocks.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import type { NullableUnions } from "./nullable-unions.js"; @@ -40,6 +40,10 @@ import { } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; +/** SCR_CYC_MAX_TYPES (scr_rc.h): the registry's size, and the length of + * the young-in nursery's per-id skip countdown. */ +const CYC_MAX_TYPES = 32768; + /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { @@ -101,6 +105,67 @@ export interface ShapeHost { * in emission order. * Absent: every descriptor gets 0, which keeps no statistics. */ nextCycKind?(): number; + /** The young-in nursery (backend/young-nursery.ts): `new` hands each + * traced program-owned instance to scr_yin_enter. Absent: off. */ + readonly youngNursery?: boolean; +} + +/** The nursery entry of a new traced instance in `%o`, after its count is + * stored; nothing when the nursery is off. Where the allocator is inlined + * (64-bit, process-global state) the entry is inline too: a type that is + * skipping the nursery (its scr_yin_skip countdown is nonzero) counts down + * and stays out; otherwise, while the nursery has room (n < lim in ScrYin, + * scr_rc.h), list the object at index n and mark its header young + * (generation SCR_CYC_YOUNG, `buf` the buffered bit and the index), and + * call scr_yin_enter_slow when it has none (it collects the nursery + * first). Other targets call scr_yin_enter, which does all of it. + * Labels use the `ye.` prefix; the lines end in an open block. */ +export function youngEntryLines(host: ShapeHost): string[] { + if (host.youngNursery !== true) return []; + if (host.inlineAlloc !== true || host.threadLocalAlloc === true) { + host.declare(`declare void @scr_yin_enter(ptr)`); + return [` call void @scr_yin_enter(ptr %o) ; the young-in nursery`]; + } + host.declare(`@scr_yin = external global { ptr, i64, i64, i64 }`); + host.declare(`@scr_yin_skip = external global [${CYC_MAX_TYPES} x i32]`); + host.declare(`declare void @scr_yin_enter_slow(ptr)`); + return [ + ` %ye.idp = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR} ; the young-in nursery`, + ` %ye.id16 = load i16, ptr %ye.idp ; ScrCycHdr.id`, + ` %ye.id = zext i16 %ye.id16 to i64`, + ` %ye.sp = getelementptr inbounds [${CYC_MAX_TYPES} x i32], ptr @scr_yin_skip, i64 0, i64 %ye.id`, + ` %ye.s = load i32, ptr %ye.sp`, + ` %ye.skip = icmp ne i32 %ye.s, 0`, + ` br i1 %ye.skip, label %ye.skipped, label %ye.enter`, + `ye.skipped: ; a type that does not die young: counted down, stays out`, + ` %ye.s1 = sub i32 %ye.s, 1`, + ` store i32 %ye.s1, ptr %ye.sp`, + ` br label %ye.done`, + `ye.enter:`, + ` %ye.v = load ptr, ptr @scr_yin`, + ` %ye.np = getelementptr inbounds i8, ptr @scr_yin, i64 8`, + ` %ye.n = load i64, ptr %ye.np`, + ` %ye.lp = getelementptr inbounds i8, ptr @scr_yin, i64 16`, + ` %ye.l = load i64, ptr %ye.lp`, + ` %ye.room = icmp ult i64 %ye.n, %ye.l`, + ` br i1 %ye.room, label %ye.fast, label %ye.slow`, + `ye.fast:`, + ` %ye.slot = getelementptr inbounds ptr, ptr %ye.v, i64 %ye.n`, + ` store ptr %o, ptr %ye.slot`, + ` %ye.n1 = add i64 %ye.n, 1`, + ` store i64 %ye.n1, ptr %ye.np`, + ` %ye.genp = getelementptr inbounds i8, ptr %o, i64 -5`, + ` store i8 3, ptr %ye.genp ; ScrCycHdr.gen = SCR_CYC_YOUNG`, + ` %ye.bufp = getelementptr inbounds i8, ptr %o, i64 -4`, + ` %ye.i = trunc i64 %ye.n to i32`, + ` %ye.buf = or i32 %ye.i, -2147483648`, + ` store i32 %ye.buf, ptr %ye.bufp ; ScrCycHdr.buf = buffered bit | index`, + ` br label %ye.done`, + `ye.slow:`, + ` call void @scr_yin_enter_slow(ptr %o)`, + ` br label %ye.done`, + `ye.done:`, + ]; } /** What class emission asks of lifetime regions (LifetimeRegions in @@ -1453,6 +1518,7 @@ export function emitRecordShapes( ` store ptr %ovf, ptr %ovfp`, ); } + if (traced) nw.push(...youngEntryLines(host)); nw.push(` ret ptr %o`, `}`, ``); defs.push(...nw); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 8f69cea57..3beb84a20 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -16,6 +16,9 @@ import type { NativeTargetSpec } from "./targets.js"; export interface NativeLinkFeatures { workers?: boolean; + /** The young-in nursery (backend/young-nursery.ts): the SCR_YIN + * variant of the collector unit. Absent when off. */ + youngNursery?: boolean; regex: boolean; copying: boolean; textDecoderLegacy: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 5e981fe44..875a723bc 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -46,6 +46,8 @@ export interface CcOptions { frameworks?: readonly string[]; /** Isolated native Worker contexts; every runtime object must use TLS. */ workers?: boolean; + /** Compile the runtime with SCR_YIN (backend/young-nursery.ts). */ + youngNursery?: boolean; regex?: boolean; /** The program uses one of the copying/typed-array bridge intrinsics diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 549e9ebbf..6c8673fe4 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -445,6 +445,7 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", // The runtime object model uses type-punned C: a hierarchy // upcast is a raw pointer cast, so one object's header (rc, vt) and @@ -643,6 +644,7 @@ export async function compileCInternal( : [optimization === "dev" ? "-O0" : "-O2"]), ...executableSectionFlags.compile, ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []), + ...(opts.youngNursery ? ["-DSCR_YIN"] : []), "-fno-math-errno", "-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs "-Wno-deprecated-declarations", diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index 039ecc209..16fbc2a36 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -80,6 +80,7 @@ export interface RuntimePackManifest { export interface RuntimeFeatureSet extends NativeLinkFeatures { workers: boolean; + youngNursery: boolean; eventTarget: boolean; nativeFetch: boolean; netEffective: boolean; @@ -266,6 +267,7 @@ export function effectiveRuntimeFeatures( return { ...features, workers: features.workers === true, + youngNursery: features.youngNursery === true, eventTarget: features.eventTarget === true, nativeFetch, netEffective, diff --git a/packages/compiler/src/backend/young-nursery.ts b/packages/compiler/src/backend/young-nursery.ts new file mode 100644 index 000000000..090a4a6b9 --- /dev/null +++ b/packages/compiler/src/backend/young-nursery.ts @@ -0,0 +1,9 @@ +/** The young-in nursery (phase 1, experimental): compile-time opt-in with + * SCRIPTC_YOUNG_NURSERY=1. Executables then allocate compiled class and + * record instances young (the emitted constructors call scr_yin_enter) and + * link the collector unit built with SCR_YIN, which collects young cycles + * in nursery collections (scr_cycle.c, "young-in nursery"). Unset, nothing + * the compiler emits or links changes. */ +export function youngNurseryRequested(env: NodeJS.ProcessEnv = process.env): boolean { + return env["SCRIPTC_YOUNG_NURSERY"] === "1"; +} diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index fbd7e4edd..ee9d84fcf 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -49,6 +49,7 @@ export interface EarlyExecutableNativeFeatures { /** Omitted is the historical release posture. */ optimization?: "dev" | "speed"; workers?: boolean; + youngNursery?: boolean; regex: boolean; copying: boolean; textDecoderLegacy: boolean; @@ -111,6 +112,8 @@ export interface EarlyExecutableCacheOptions { windowsSubsystem?: "gui"; /** Omitted unless --detached-exit is set. */ detachedExit?: true; + /** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */ + youngNursery?: true; npmStatic: readonly string[] | "auto" | null; /** Raw manifest identity: path and bytes. Native archives remain under the * stricter native cache's independent dependency validation. */ @@ -200,7 +203,8 @@ function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeat (native.optimization === undefined || native.optimization === "dev" || native.optimization === "speed") && - (native.workers === undefined || typeof native.workers === "boolean"), + (native.workers === undefined || typeof native.workers === "boolean") && + (native.youngNursery === undefined || typeof native.youngNursery === "boolean"), ); } @@ -221,6 +225,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string { ...(options.strip ? ["strip"] : []), ...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []), ...(options.detachedExit ? ["detached-exit"] : []), + ...(options.youngNursery ? ["young-nursery"] : []), options.npmStatic === null ? "" : options.npmStatic === "auto" @@ -260,6 +265,7 @@ function routeKey(options: Omit; if (features["workers"] !== undefined && typeof features["workers"] !== "boolean") return false; + if (features["youngNursery"] !== undefined && typeof features["youngNursery"] !== "boolean") + return false; return [ "regex", "copying", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 9047f3376..79984d6c5 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -73,6 +73,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( systemLibraries: [], features: { workers: false, + youngNursery: false, eventTarget: false, regex: true, copying: false, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json new file mode 100644 index 000000000..b5e72c495 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-young-nursery-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5981-young-nursery-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json new file mode 100644 index 000000000..77892fe94 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-young-nursery-workers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5982-young-nursery-workers.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..858324434 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -124,6 +124,15 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + if (source === "scr_cycle.c") { + // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with + // nursery collections; programs built without it link the default. + variants.push({ + id: "young-nursery", + when: { youngNursery: true }, + defines: ["SCR_YIN"], + }); + } return variants; } diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index d5c8c3720..18ee441e6 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -343,7 +343,7 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY"], + executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/library-matrix.mjs b/packages/runtime-pack-common/library-matrix.mjs index 4f14d959b..106950941 100644 --- a/packages/runtime-pack-common/library-matrix.mjs +++ b/packages/runtime-pack-common/library-matrix.mjs @@ -27,7 +27,11 @@ export function withLibraryRuntimeFlavors(matrix) { }, ].map((unit) => ({ ...unit, - variants: unit.variants.filter((variant) => !variant.defines.includes("SCR_WORKERS")), + // Libraries run neither workers nor the young-in nursery (an + // executable-only experiment). + variants: unit.variants.filter( + (variant) => !variant.defines.includes("SCR_WORKERS") && !variant.defines.includes("SCR_YIN"), + ), })); const flavors = { ...matrix.flavors }; for (const [flavor, spec] of Object.entries(matrix.flavors)) { diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..91470b760 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -114,6 +114,15 @@ function variantsFor(source) { defines: ["SCR_TEXT_DECODER_LEGACY"], }); } + if (source === "scr_cycle.c") { + // The young-in nursery (SCRIPTC_YOUNG_NURSERY=1): the collector with + // nursery collections; programs built without it link the default. + variants.push({ + id: "young-nursery", + when: { youngNursery: true }, + defines: ["SCR_YIN"], + }); + } return variants; } diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index d1de57418..1bcd7455d 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -440,7 +440,7 @@ async function build() { ...(unitHelper === null ? {} : { speed_codegen: unitHelper.identity }), }, macros: { - executable: ["SCR_TEXT_DECODER_LEGACY"], + executable: ["SCR_TEXT_DECODER_LEGACY", "SCR_YIN"], excluded: ["SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required", }, diff --git a/packages/runtime-pack-common/test/library-matrix.test.ts b/packages/runtime-pack-common/test/library-matrix.test.ts index e78a2260c..1bebe159a 100644 --- a/packages/runtime-pack-common/test/library-matrix.test.ts +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -61,6 +61,23 @@ test("library pack modes preserve pure runtime features and exclude ambient even expect(matrix.runtime_units.some((unit) => unit.source === "scr_library.c")).toBe(false); }); +test("the young-in nursery collector is an executable-only variant of scr_cycle.c", () => { + const ids = (units: { source: string; variants: { id: string }[] }[]) => + units.find((unit) => unit.source === "scr_cycle.c")!.variants.map((variant) => variant.id); + expect(ids(matrix.runtime_units)).toEqual([ + "default", + "default-workers", + "young-nursery", + "young-nursery-workers", + ]); + const young = matrix.runtime_units + .find((unit) => unit.source === "scr_cycle.c") + .variants.find((variant) => variant.id === "young-nursery"); + expect(young).toMatchObject({ when: { youngNursery: true, workers: false }, defines: ["SCR_YIN"] }); + for (const mode of ["library-release", "library-thread-release"]) + expect(ids(matrix.flavors[mode].runtime_units)).toEqual(["default"]); +}); + test("mobile packs ship only library modes and their permitted vendor archives", () => { const mobile = createRuntimePackMatrix({ target: { object_format: "elf" }, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index c2be13a03..853545114 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -409,6 +409,22 @@ static SCR_TL unsigned scr_gen_limit = SCR_CYC_OLD; #endif static size_t scr_cyc_pass(unsigned gen_limit); +#ifdef SCR_YIN +/* The young-in nursery's state (see "young-in nursery" below): the nursery + * itself (scr_rc.h), every live young object at the index its header + * records; a collection's mark worklist, dead set and deferred releases; + * per-object counts of references from other young objects, valid during a + * collection. */ +SCR_TL ScrYin scr_yin = {0}; +static SCR_TL ScrVec scr_yin_stack; +static SCR_TL ScrVec scr_yin_dead; +static SCR_TL ScrVec scr_yin_xrel; +static SCR_TL uint32_t *scr_yin_counts; +static SCR_TL size_t scr_yin_counts_cap; +static SCR_TL bool scr_yin_busy; +static void scr_yin_leave(ScrCycHdr *h, void *obj); +static size_t scr_yin_collect(void); +#endif /* A limited pass (scr_cyc_collect_set) takes only the candidates in this * set from the buffers up to `scr_cyc_take_gen`; NULL takes them all. */ static SCR_TL const ScrCycSet *scr_cyc_take = NULL; @@ -699,6 +715,12 @@ static SCR_CYC_SCHEDULING void scr_cyc_collect_due(void) { * per turn would walk the whole live heap every turn — which is exactly the * cost the generations exist to avoid. */ void scr_cyc_collect_scheduled(void) { +#ifdef SCR_YIN + /* A collection point also collects the nursery when anything entered it + * since its last collection, so young garbage does not wait for the + * allocation trigger in a program that has gone idle. */ + if (scr_yin.n != 0) scr_yin_collect(); +#endif if (scr_cyc_buffered() == 0) { scr_cyc_scheduled_mature_age = 0; return; @@ -834,6 +856,13 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) { void scr_cyc_on_dead(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); if (!scr_cyc_is_buffered(h)) return; +#ifdef SCR_YIN + /* A young object leaves the nursery; nothing else lists it. */ + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) { + scr_yin_leave(h, obj); + return; + } +#endif /* O(1) removal: swap the last entry into the hole. A buffered object's * generation never changes (promotion runs only on objects the pass has * already drained from the buffers), so `gen` names the right one. */ @@ -863,6 +892,18 @@ static SCR_TL ScrVec scr_pub_holes[SCR_CYC_NGENS]; uint32_t scr_cyc_pub_take(void *obj) { ScrCycHdr *h = scr_cyc_hdr(obj); +#ifdef SCR_YIN + /* A young object (its header reads as buffered) leaves the nursery, + * unbuffered: young objects never become immortal in the nursery, and so + * never reach another thread from it. The answer UINT32_MAX makes + * scr_pub_mark's recorded position 0 ("not taken"), so a refused publish + * leaves it out of every buffer, as it was. It is reachable from the + * graph being published, so it needs no candidate. */ + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) { + scr_yin_leave(h, obj); + return UINT32_MAX; + } +#endif unsigned gen = scr_cyc_gen(h); uint32_t index = scr_cyc_buf_index(h); scr_roots[gen].v[index] = NULL; @@ -931,9 +972,19 @@ void scr_cyc_on_release(void *obj) { * two must agree on which edges are internal. Nothing to do for NULL * (unassigned slots), immortal (interned statics have no header at all), or * a generation this pass does not collect. */ +#ifdef SCR_YIN +/* Trial deletion walks a young object as a nursery-level one: structures + * that mix young objects with families that are not young (arrays, + * closures) are found by the candidates of the latter, exactly as before. + * A young survivor stays young (scan_black promotes only below OLD) and a + * young member of the dead set leaves the nursery before its teardown. */ +#define SCR_CYC_WALK_GEN(h) (scr_cyc_gen(h) == SCR_CYC_YOUNG ? SCR_CYC_NURSERY : scr_cyc_gen(h)) +#else +#define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h) +#endif #define SCR_CYC_SKIP(child) \ ((child) == NULL || SCR_RC(child) == SIZE_MAX \ - || scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit) + || SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit) /* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where * an object is queued instead (scr_pending, or scr_restore_pending for the @@ -976,7 +1027,7 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp static void scr_mg_visit(void *child, void *ctx) { if (child == NULL || SCR_RC(child) == SIZE_MAX) return; ScrCycHdr *h = scr_cyc_hdr(child); - if (scr_cyc_gen(h) > scr_gen_limit) { + if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) { scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder); scr_cyc_push_work(&scr_xgen_edges, child); return; @@ -1148,6 +1199,333 @@ static SCR_CYC_COMPACT void scr_xg_release(void *obj) { scr_rc_destroy(obj, scr_xg_destroy); } +#ifdef SCR_YIN +/* ── young-in nursery ─────────────────────────────────────────────────── */ + +/* Programs built with the young-in nursery (SCRIPTC_YOUNG_NURSERY=1, which + * links this unit built with SCR_YIN) allocate compiled class and record + * instances YOUNG: the emitted constructor hands each new object to + * scr_yin_enter, which lists it in the nursery and marks its header + * SCR_CYC_YOUNG with its nursery index in `buf`, under the buffered bit. + * The bit makes every release path treat the object as an existing + * candidate: a release that leaves it alive makes no call, and its death + * calls scr_cyc_on_dead, which takes it out of the nursery + * (scr_yin_leave) before the teardown runs. Young objects are never + * candidates, but trial deletion walks them as nursery-level objects when + * a candidate reaches them (SCR_CYC_WALK_GEN). + * + * Whenever the nursery holds SCR_YOUNG_NURSERY_SIZE live objects (4096 by + * default), at collection points and before every full sweep, the nursery + * is collected on its own, in time proportional to its size and without + * walking anything older: + * count for every young object, the references to it from traced + * fields of other young objects (one trace per young object); + * mark an object whose count is below its reference count is + * referenced from outside the nursery (a stack, a global, an + * older object, a runtime structure, FFI, a fiber or a coroutine + * frame: every one of them holds a counted reference); mark + * from each such object through young edges; + * sweep what is unmarked is referenced only from unmarked young + * objects, which is garbage. Marked objects join trial + * deletion's nursery generation, unbuffered; dead ones are torn + * down, giving up only the edges that leave the dead set (like + * the white set's cross-generation edges, after every teardown). + * Survivors go to trial deletion's nursery rather than past it because the + * structures that survive are mostly held by families that are not young + * (arrays, closures, maps): a dead tree whose nodes are young and whose + * child arrays are not survives the nursery collection through the + * arrays, and the arrays' own candidates reach it there. + * Soundness rests on the two invariants trial deletion already needs: every + * strong reference is counted, and a trace visits exactly the counted + * references to headered children. The counts are recomputed at every + * collection, so no store barrier is needed and none can be missed. + * + * Candidates. A release of a young object never buffers it, so a structure + * whose last outside reference was dropped while its root was young has no + * candidate, and when the structure reaches beyond the nursery (a young + * node in an array that is not young, say), the nursery collection sees + * the outside edge as a root and keeps it. So every outside root a + * collection finds (count below the reference count) becomes a candidate + * as it is promoted. That suffices: every survivor is reachable from such + * a root, so if a survivor was already garbage, so was its root, and trial + * deletion reaches the survivor from the root's candidate; garbage formed + * after the promotion has a candidate from the release that formed it, + * since survivors are no longer young. */ +static void scr_cyc_trim(ScrVec *vec); +#define SCR_YIN_DEFAULT_SIZE 4096 +#define SCR_CYC_YIN_PROMOTED SCR_CYC_NURSERY + +/* Pushes onto the collection's work vectors, inline. */ +static inline __attribute__((always_inline)) void scr_yin_push(ScrVec *vec, void *obj) { + if (__builtin_expect(vec->n == vec->cap, 0)) scr_cyc_grow(vec); + vec->v[vec->n++] = obj; + if (vec->n > vec->used) vec->used = vec->n; +} +#define SCR_YIN_MAX_SIZE ((size_t)1 << 30) + +static size_t scr_yin_size(void) { + static SCR_TL size_t cached = 0; + if (cached == 0) { + const char *env = scr_getenv("SCR_YOUNG_NURSERY_SIZE"); + long v = env ? strtol(env, NULL, 10) : 0; + cached = v > 0 ? ((size_t)v < SCR_YIN_MAX_SIZE ? (size_t)v : SCR_YIN_MAX_SIZE) + : SCR_YIN_DEFAULT_SIZE; + } + return cached; +} + +/* Collections, survivors and dead objects, printed at exit with + * SCR_YOUNG_NURSERY_STATS=1 (measurement). */ +static SCR_TL size_t scr_yin_stat[3]; +static void scr_yin_stats_print(void) { + fprintf(stderr, "scriptc young nursery: %zu collections, %zu survivors, %zu dead\n", + scr_yin_stat[0], scr_yin_stat[1], scr_yin_stat[2]); +} + +/* Types that do not die in young cycles. Only a young object that dies in + * a nursery collection gains anything there; every other one costs: one + * that dies by reference counting makes a call to leave the nursery (where + * it made none before), one that is published is moved out of it, and one + * that survives a collection is kept by it, and becomes a trial-deletion + * candidate when an outside reference holds it. Structures held by + * families that are not young (tree nodes in child arrays, objects + * captured by closures) survive, and plain data dies by reference counting. + * So each thread keeps, per registry id, how many of the type's young + * objects left the nursery (by a collection or otherwise) and how many of + * them a collection freed, and after SCR_YIN_WINDOW of them a type freed + * for fewer than one in SCR_YIN_RATIO is allocated as before (trial + * deletion's nursery) for its next SCR_YIN_PROBE allocations, then + * enters again for a fresh window: the same bypass-and-probe scheme as the + * levels' bypass above. The countdown + * (scr_yin_skip, an ABI: emitted constructors test and decrement it + * inline) only steers which objects enter the nursery. */ +#define SCR_YIN_WINDOW 1024 +#define SCR_YIN_RATIO 4 +#define SCR_YIN_PROBE (64 * SCR_YIN_WINDOW) +#ifdef SCR_WORKERS +static _Thread_local uint32_t *scr_yin_skip_tl; +static uint32_t *scr_yin_skip_table(void) { + if (!scr_yin_skip_tl) { + scr_yin_skip_tl = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_skip_tl); + if (!scr_yin_skip_tl) scr_cyc_oom(); + } + return scr_yin_skip_tl; +} +#else +uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES] = {0}; +static uint32_t *scr_yin_skip_table(void) { return scr_yin_skip; } +#endif +typedef struct { + uint32_t met, freed; +} ScrYinType; +/* SCR_YOUNG_NURSERY_ALL=1 keeps every type young (test lanes). */ +static bool scr_yin_all(void) { + static SCR_TL int cached = -1; + if (cached < 0) { + const char *env = scr_getenv("SCR_YOUNG_NURSERY_ALL"); + cached = env != NULL && env[0] == '1'; + } + return cached != 0; +} +static SCR_TL ScrYinType *scr_yin_types; + +static __attribute__((noinline, cold)) void scr_yin_decide(uint16_t id, ScrYinType *t) { + if (t->freed * SCR_YIN_RATIO < t->met && !scr_yin_all()) scr_yin_skip_table()[id] = SCR_YIN_PROBE; + t->met = t->freed = 0; +} +static inline __attribute__((always_inline)) void scr_yin_met(uint16_t id, bool dead) { + ScrYinType *t = &scr_yin_types[id]; + t->freed += dead; + if (__builtin_expect(++t->met >= SCR_YIN_WINDOW, 0)) scr_yin_decide(id, t); +} + +void scr_yin_enter_slow(void *obj); + +/* The whole nursery entry, for targets whose constructors call it. */ +void scr_yin_enter(void *obj) { + uint32_t *skip = &scr_yin_skip_table()[scr_cyc_hdr(obj)->id]; + if (*skip != 0) { + --*skip; + return; + } + scr_yin_enter_slow(obj); +} + +/* The slow half of the nursery entry (the emitted constructors inline the + * rest: skip, or append while n < lim). The nursery holds at most + * SCR_YOUNG_NURSERY_SIZE live young objects: the object that would exceed + * that collects the nursery first, then enters. */ +void scr_yin_enter_slow(void *obj) { + size_t size = scr_yin_size(); + if (!scr_yin_types) { + scr_yin_types = calloc(SCR_CYC_MAX_TYPES, sizeof *scr_yin_types); + if (!scr_yin_types) scr_cyc_oom(); + } + if (scr_yin.n >= size) { + static SCR_TL bool stats_checked = false; + if (!stats_checked) { + stats_checked = true; + const char *env = scr_getenv("SCR_YOUNG_NURSERY_STATS"); + if (env && env[0] == '1') atexit(scr_yin_stats_print); + } + scr_yin_collect(); + } + if (scr_yin.n == scr_yin.cap) { + /* Positions are 31-bit (ScrCycHdr.buf); the size is capped below. */ + size_t cap = scr_yin.cap ? scr_yin.cap * 2 : 1024; + void **grown = realloc(scr_yin.v, cap * sizeof *grown); + if (!grown) scr_cyc_oom(); + scr_yin.v = grown; + scr_yin.cap = cap; + } + scr_yin.lim = scr_yin.cap < size ? scr_yin.cap : size; + ScrCycHdr *h = scr_cyc_hdr(obj); + scr_cyc_set_gen(h, SCR_CYC_YOUNG); + scr_cyc_set_buffered(h, (uint32_t)scr_yin.n); + scr_yin.v[scr_yin.n++] = obj; +} + +/* O(1) removal: the last young object takes the hole. */ +static void scr_yin_leave(ScrCycHdr *h, void *obj) { + size_t i = scr_cyc_buf_index(h); +#ifdef SCR_RC_AUDIT + if (i >= scr_yin.n || scr_yin.v[i] != obj) scr_trap("scriptc: young object outside the nursery\n"); +#endif + void *last = scr_yin.v[--scr_yin.n]; + scr_yin.v[i] = last; + if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i); + scr_cyc_clear_buffered(h); + scr_cyc_set_gen(h, SCR_CYC_NURSERY); + scr_yin_met(h->id, false); +} + +static void scr_yin_count_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++; +} + +/* Marked young objects are GRAY (young objects are otherwise BLACK). */ +static void scr_yin_mark_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return; + scr_cyc_set_color(h, SCR_CYC_GRAY); + scr_yin_push(&scr_yin_stack, child); +} + +/* A dead object's edge: young and unmarked is dead too (nothing to give + * up); a marked young object (promoted now or soon) or an older one keeps + * the count until every teardown has run. */ +static void scr_yin_dead_visit(void *child, void *ctx) { + (void)ctx; + if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + ScrCycHdr *h = scr_cyc_hdr(child); + if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return; + scr_yin_push(&scr_yin_xrel, child); +} + +/* Fully optimized, unlike trial deletion's per-pass bookkeeping: its loops + * run once per young object. */ +static __attribute__((noinline)) size_t scr_yin_collect(void) { + if (scr_yin_busy || scr_collecting) return 0; + size_t n = scr_yin.n; + if (n == 0) return 0; + scr_yin_busy = true; + /* Releases made by teardowns below buffer candidates but must not start + * a trial-deletion pass while the dead set is half torn down. */ + scr_collecting = true; + if (n > scr_yin_counts_cap) { + size_t cap = scr_yin_counts_cap ? scr_yin_counts_cap * 2 : 1024; + while (cap < n) cap *= 2; + uint32_t *counts = realloc(scr_yin_counts, cap * sizeof *counts); + if (!counts) scr_cyc_oom(); + scr_yin_counts = counts; + scr_yin_counts_cap = cap; + } + memset(scr_yin_counts, 0, n * sizeof *scr_yin_counts); + void **v = scr_yin.v; + for (size_t k = 0; k < n; k++) + scr_cyc_type(scr_cyc_hdr(v[k]))->trace(v[k], scr_yin_count_visit, NULL); +#ifdef SCR_RC_AUDIT + /* Membership and counts: every listed object is young at its index and + * alive, and holds at least the references young objects make to it + * (fewer would mean an uncounted reference). */ + for (size_t k = 0; k < n; k++) { + ScrCycHdr *h = scr_cyc_hdr(v[k]); + if (scr_cyc_gen(h) != SCR_CYC_YOUNG || !scr_cyc_is_buffered(h) || scr_cyc_buf_index(h) != k || + scr_cyc_color(h) != SCR_CYC_BLACK) + scr_trap("scriptc: corrupt young nursery entry\n"); + if (SCR_RC(v[k]) == 0 || SCR_RC(v[k]) < scr_yin_counts[k]) + scr_trap("scriptc: young object with uncounted references\n"); + } +#endif + for (size_t k = 0; k < n; k++) { + void *obj = v[k]; + ScrCycHdr *h = scr_cyc_hdr(obj); + if (scr_cyc_color(h) == SCR_CYC_GRAY || SCR_RC(obj) <= scr_yin_counts[k]) continue; + scr_cyc_set_color(h, SCR_CYC_GRAY); + scr_yin_push(&scr_yin_stack, obj); + while (scr_yin_stack.n) { + void *x = scr_yin_stack.v[--scr_yin_stack.n]; + scr_cyc_type(scr_cyc_hdr(x))->trace(x, scr_yin_mark_visit, NULL); + } + } + /* Split: promote the marked objects; record the dead ones and their + * edges to survivors and older objects, which are given up after every + * teardown (until then each holds the count it was made with, so no + * teardown's releases can free a target early). Edges between dead + * objects need nothing: both go. No block is freed before every dead + * object has been traced. */ + scr_yin_dead.n = 0; + scr_yin_xrel.n = 0; + for (size_t k = 0; k < n; k++) { + void *obj = v[k]; + ScrCycHdr *h = scr_cyc_hdr(obj); + if (scr_cyc_color(h) == SCR_CYC_GRAY) { + scr_cyc_set_color(h, SCR_CYC_BLACK); + scr_cyc_set_gen(h, SCR_CYC_YIN_PROMOTED); + scr_cyc_clear_buffered(h); + /* An outside root becomes a candidate (see "candidates" above). */ + if (SCR_RC(obj) > scr_yin_counts[k]) scr_cyc_on_release(obj); + scr_yin_met(h->id, false); + } else { + scr_yin_met(h->id, true); + scr_cyc_set_color(h, SCR_CYC_DOOMED); + scr_yin_push(&scr_yin_dead, obj); + scr_cyc_type(h)->trace(obj, scr_yin_dead_visit, NULL); + } + } + scr_yin.n = 0; + scr_yin_stat[0]++; + scr_yin_stat[1] += n - scr_yin_dead.n; + scr_yin_stat[2] += scr_yin_dead.n; + size_t freed = scr_yin_dead.n; + for (size_t i = 0; i < scr_yin_dead.n; i++) { + void *obj = scr_yin_dead.v[i]; + scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj); + } + scr_yin_dead.n = 0; + scr_xg_freed = 0; + for (size_t i = 0; i < scr_yin_xrel.n; i++) scr_xg_release(scr_yin_xrel.v[i]); + freed += scr_xg_freed; + scr_yin_xrel.n = 0; + scr_cyc_trim(&scr_yin_stack); + scr_cyc_trim(&scr_yin_dead); + scr_cyc_trim(&scr_yin_xrel); + scr_collecting = false; + scr_yin_busy = false; + /* The candidates buffered above count toward trial deletion's trigger, + * which only a release checks; a program whose releases are all of + * young objects would otherwise never reach it. */ + if (scr_cyc_nbuffered >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger) + scr_cyc_collect_due(); + return freed; +} +#endif + /* One pass over every candidate at or below `gen_limit`; returns how many * objects it freed. */ /* The second half of a pass: settle the white set, promote, re-buffer, @@ -1242,6 +1620,9 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) { const ScrCycType *type = scr_cyc_type(h); if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(type, gen_limit); else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE; +#ifdef SCR_YIN + if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_leave(h, obj); +#endif type->free_fn(obj); } scr_white.n = 0; @@ -1492,9 +1873,16 @@ size_t scr_cyc_pass_at(unsigned gen) { return scr_cyc_pass(gen); } size_t scr_cyc_collect_set(const ScrCycSet *set, unsigned gen) { if (scr_collecting) return 0; +#ifdef SCR_YIN + /* Young objects are invisible to trial deletion: collect them first, so + * survivors are promoted where the limited pass can walk them. */ + size_t young = scr_yin_collect(); +#else + size_t young = 0; +#endif scr_cyc_take = set; scr_cyc_take_gen = gen; - size_t freed = scr_cyc_pass(SCR_CYC_OLD); + size_t freed = young + scr_cyc_pass(SCR_CYC_OLD); scr_cyc_take = NULL; return freed; } @@ -1508,6 +1896,11 @@ void scr_collect_cycles(void) { * can have buffered anything new, so a pass that comes up empty ends * this; and since each repeat needs a strictly smaller live heap to * continue, it terminates. */ +#ifdef SCR_YIN + /* The nursery first: afterwards no young object is left (a teardown + * never allocates one), so the passes see every object. */ + scr_yin_collect(); +#endif while (scr_cyc_pass(SCR_CYC_OLD) && scr_cyc_buffered()) { } } @@ -1526,4 +1919,22 @@ void scr_cyc_context_cleanup(void) { free(scr_cyc_kinds); scr_cyc_kinds = NULL; scr_cyc_kinds_cap = 0; +#ifdef SCR_YIN + ScrVec *young[] = {&scr_yin_stack, &scr_yin_dead, &scr_yin_xrel}; + for (size_t i = 0; i < sizeof young / sizeof young[0]; i++) { + free(young[i]->v); + *young[i] = (ScrVec){0}; + } + free(scr_yin.v); + scr_yin = (ScrYin){0}; + free(scr_yin_types); + scr_yin_types = NULL; +#ifdef SCR_WORKERS + free(scr_yin_skip_tl); + scr_yin_skip_tl = NULL; +#endif + free(scr_yin_counts); + scr_yin_counts = NULL; + scr_yin_counts_cap = 0; +#endif } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index d5d83a72a..8de5fa49d 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -524,4 +524,36 @@ SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); SCR_COLD void scr_throw_published_collection(bool set); +/* Young-in nursery (scr_cycle.c built with SCR_YIN, linked by programs + * compiled with SCRIPTC_YOUNG_NURSERY=1). A young object's header has + * generation SCR_CYC_YOUNG, above every trial-deletion level, and its + * nursery index in `buf` under SCR_CYC_BUFFERED_BIT, so release paths treat + * it as a buffered candidate (no call on a release that leaves it alive, + * scr_cyc_on_dead at its death). Emitted constructors of compiled classes + * and records call scr_yin_enter on each new object, after storing its + * count; the call may collect the nursery first. */ +#define SCR_CYC_YOUNG 3 +void scr_yin_enter(void *obj); +/* The inline entry's halves: the per-registry-id countdown of allocations + * that skip the nursery (process-wide in executables without workers, + * which are the ones that inline the entry), and the append when the + * nursery has no room. */ +extern uint32_t scr_yin_skip[SCR_CYC_MAX_TYPES]; +void scr_yin_enter_slow(void *obj); +/* The nursery: live young objects v[0..n), capacity cap. Its layout is an + * ABI: emitted constructors (llvm/shapes.ts youngEntryLines) append inline + * while n < lim, writing the header themselves, and call scr_yin_enter + * otherwise; lim stays at most the capacity and the collection size. */ +typedef struct ScrYin { + void **v; + size_t n; + size_t lim; + size_t cap; +} ScrYin; +extern SCR_TL ScrYin scr_yin; +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrYin, n) == 8 && offsetof(ScrYin, lim) == 16, + "llvm/shapes.ts appends to the nursery at these offsets"); +#endif + #endif /* SCR_RC_H */ diff --git a/packages/runtime/test/test_yin.c b/packages/runtime/test/test_yin.c new file mode 100644 index 000000000..3b746a435 --- /dev/null +++ b/packages/runtime/test/test_yin.c @@ -0,0 +1,442 @@ +/* The young-in nursery (scr_cycle.c built with SCR_YIN): young objects + * allocated as emitted constructors allocate them, released as emitted + * release paths release them, and collected by nursery collections, trial + * deletion and the full sweep. Every check must hold for any nursery size + * (SCR_YOUNG_NURSERY_SIZE), including a one-object nursery that collects at + * every allocation. */ +#include "../src/scr_runtime.h" + +#include +#include +#include + +typedef struct Plain { + size_t rc; +} Plain; + +typedef struct Obj Obj; +struct Obj { + size_t rc; + Obj *a; + Obj *b; + Plain *plain; /* untraced: released by the teardown, never visited */ +}; + +static size_t freed, plain_freed, disposed; + +static void check(bool condition, const char *message) { + if (condition) return; + fprintf(stderr, "young nursery test failed: %s\n", message); + exit(EXIT_FAILURE); +} + +_Noreturn void scr_trap(const char *msg) { + fputs(msg, stderr); + exit(EXIT_FAILURE); +} + +size_t scr_cyc_pass_at(unsigned gen); + +static void plain_release(Plain *p) { + if (p && --p->rc == 0) { + plain_freed++; + free(p); + } +} + +static void obj_trace(void *o, ScrTraceVisit visit, void *ctx) { + Obj *x = o; + if (x->a) visit(x->a, ctx); + if (x->b) visit(x->b, ctx); +} + +/* The collector's teardown: untraced children only, then the block. */ +static void obj_gcfree(void *o) { + plain_release(((Obj *)o)->plain); + freed++; + scr_cyc_free(o); +} + +static ScrCycType young_type = {&obj_trace, &obj_gcfree, 0, 0}; +static ScrCycType mature_type = {&obj_trace, &obj_gcfree, 0, 0}; + +/* An emitted constructor: zeroed headered block, rc = 1, then the nursery. */ +static Obj *young_new_of(ScrCycType *type) { + Obj *o = scr_cyc_alloc(sizeof(Obj), type); + o->rc = 1; + scr_yin_enter(o); + return o; +} +static Obj *young_new(void) { return young_new_of(&young_type); } + +/* A runtime family that is not young (gen 0, trial deletion only). */ +static Obj *mature_new(void) { + Obj *o = scr_cyc_alloc(sizeof(Obj), &mature_type); + o->rc = 1; + return o; +} + +static void release(Obj *o); +static void destroy(void *p) { + Obj *o = p; + release(o->a); + release(o->b); + plain_release(o->plain); + freed++; + scr_cyc_free(o); +} + +/* An emitted release: the buffered-bit tests are what young objects rely on. */ +static void release(Obj *o) { + if (!o || o->rc == SIZE_MAX) return; + if (--o->rc == 0) { + scr_cyc_dead(o); + scr_rc_destroy(o, destroy); + return; + } + scr_cyc_released(o); +} + +static Obj *retain(Obj *o) { + if (o && o->rc != SIZE_MAX) o->rc++; + return o; +} + +/* h->a or h->b = v (v borrowed): unlink, then release the old value. */ +static void store(Obj *h, int slot, Obj *v) { + retain(v); + Obj **f = slot == 0 ? &h->a : &h->b; + Obj *old = *f; + *f = v; + release(old); +} + +static unsigned gen_of(Obj *o) { return scr_cyc_gen(scr_cyc_hdr(o)); } +static bool buffered(Obj *o) { return scr_cyc_is_buffered(scr_cyc_hdr(o)); } + +static void count_dispose(void *obj) { + (void)obj; + disposed++; +} + +/* Everything collectable is collected and the live count is back. */ +static void settle(size_t live, const char *what) { + scr_collect_cycles(); + if (scr_cyc_live != live) { + fprintf(stderr, "live %zu, expected %zu\n", scr_cyc_live, live); + check(false, what); + } +} + +/* With a nursery of a few objects, a pair's first object is promoted before + * the pair is linked, and the pair then waits for an older level's pass: + * the exact checks need room for the pair. */ +static bool small_nursery(void) { + const char *size = getenv("SCR_YOUNG_NURSERY_SIZE"); + return size != NULL && atol(size) < 4; +} + +static void check_dead_pair_and_live_ring(void) { + size_t live = scr_cyc_live, before = freed; + Obj *a = young_new(), *b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + check(freed == before, "a young cycle died by reference counting"); + + enum { RING = 10 }; + Obj *head = young_new(), *last = retain(head); + for (int i = 1; i < RING; i++) { + Obj *n = young_new(); + store(last, 0, n); + release(last); + last = n; + } + store(last, 0, head); + release(last); + settle(live + RING, "dead young pair not collected or live ring freed"); + check(freed == before + 2, "dead young pair not freed exactly"); + check(gen_of(head) != SCR_CYC_YOUNG && !buffered(head), "survivor not promoted unbuffered"); + release(head); /* an older release buffers the ring (and may collect it) */ + settle(live, "promoted ring not collected after its last reference"); + check(freed == before + 2 + RING, "promoted ring not freed exactly"); +} + +static void check_rc_deaths_leave_the_nursery(void) { + enum { N = 300 }; + size_t live = scr_cyc_live, before = freed; + Obj *objs[N]; + for (int i = 0; i < N; i++) objs[i] = young_new(); + /* Pairs (i, i+1) for even i; every third object dies by RC first. */ + for (int i = 0; i + 1 < N; i += 2) { + store(objs[i], 0, objs[i + 1]); + store(objs[i + 1], 0, objs[i]); + } + size_t rc_dead = 0; + for (int i = 0; i < N; i += 6) { + /* break the pair, then drop both: plain RC deaths of young objects */ + store(objs[i], 0, NULL); + store(objs[i + 1], 0, NULL); + release(objs[i]); + release(objs[i + 1]); + objs[i] = objs[i + 1] = NULL; + rc_dead += 2; + } + check(freed == before + rc_dead, "young RC deaths not freed at once"); + /* Keep every fourth surviving pair alive from outside; drop the rest. */ + Obj *kept[N]; + size_t nkept = 0, dropped = 0; + for (int i = 0; i < N; i += 2) { + if (!objs[i]) continue; + if ((i / 2) % 4 == 0) kept[nkept++] = objs[i]; + else { + release(objs[i]); + dropped += 2; + } + release(objs[i + 1]); + } + settle(live + 2 * nkept, "young pairs not collected after RC deaths"); + check(freed == before + rc_dead + dropped, "dead pairs not freed exactly"); + for (size_t k = 0; k < nkept; k++) { + check(gen_of(kept[k]) != SCR_CYC_YOUNG, "kept pair not promoted"); + release(kept[k]); + } + settle(live, "kept pairs not collected after release"); +} + +static void check_dead_set_gives_up_outgoing_edges(void) { + size_t live = scr_cyc_live, before = freed, plain_before = plain_freed; + Obj *m = mature_new(); /* held from outside, and by the dead set */ + Obj *s = young_new(); /* survivor held from outside, and by the dead set */ + Obj *d1 = young_new(), *d2 = young_new(); + Plain *p = calloc(1, sizeof *p); + p->rc = 1; + d1->plain = p; + store(d1, 0, d2); + store(d2, 0, d1); + store(d1, 1, m); + store(d2, 1, s); + release(d1); + release(d2); + check(m->rc == 2 && s->rc == 2, "setup counts"); + settle(live + 2, "dead set with outgoing edges not collected"); + check(freed == before + 2, "only the dead pair is freed"); + check(plain_freed == plain_before + 1, "untraced child of the dead set not released"); + check(m->rc == 1 && s->rc == 1, "dead set's outgoing edges not given up exactly once"); + check(gen_of(s) != SCR_CYC_YOUNG, "survivor not promoted"); + release(m); + release(s); + settle(live, "outside objects not freed at their last release"); +} + +static void check_older_holders_keep_young_alive(void) { + size_t live = scr_cyc_live, before = freed; + Obj *m = mature_new(); + Obj *y1 = young_new(), *y2 = young_new(); + store(y1, 0, y2); + store(y2, 0, y1); + store(m, 0, y1); + release(y1); + release(y2); + settle(live + 3, "young cycle held by an older object was freed"); + check(freed == before, "nothing may be freed while the holder lives"); + release(m); /* m dies by RC; its release of y1 buffers the promoted cycle */ + settle(live, "cycle not collected after its older holder died"); + check(freed == before + 3, "holder and cycle not freed exactly"); +} + +static void check_trial_deletion_releases_into_the_nursery(void) { + size_t live = scr_cyc_live, before = freed; + Obj *m1 = mature_new(), *m2 = mature_new(); + Obj *y = young_new(), *z = young_new(); + store(m1, 0, m2); + store(m2, 0, m1); + store(m1, 1, y); /* y's only reference: dies when the white set goes */ + store(m2, 1, z); /* z keeps an outside reference */ + release(y); + release(m1); + release(m2); + /* A trial-deletion pass alone walks the young objects at the nursery + * level: y joins the white set (and leaves the nursery), z survives. */ + scr_cyc_pass_at(SCR_CYC_OLD); + check(freed == before + 3, "white set and its only-held young child not freed"); + check(z->rc == 1, "outside-held young child damaged"); + check(small_nursery() || gen_of(z) == SCR_CYC_YOUNG, "trial deletion promoted a young survivor"); + release(z); + settle(live, "nursery inconsistent after trial deletion freed young objects"); + check(freed == before + 4, "last young object not freed"); + + /* A young cycle held only by a dead older cycle: one pass frees all. */ + before = freed; + m1 = mature_new(); + m2 = mature_new(); + Obj *y1 = young_new(), *y2 = young_new(); + store(y1, 0, y2); + store(y2, 0, y1); + store(m1, 0, m2); + store(m2, 0, m1); + store(m1, 1, y1); + release(y1); + release(y2); + release(m1); + release(m2); + scr_cyc_pass_at(SCR_CYC_OLD); + check(freed == before + 4, "mixed dead structure not freed by one pass"); + settle(live, "nursery inconsistent after a mixed dead structure"); +} + +/* A dead structure whose root died young and that reaches beyond the + * nursery: the nursery collection keeps it (an older object holds a young + * member) and must leave a candidate, or nothing short of a full sweep + * would ever reach it. */ +static void check_dead_structures_beyond_the_nursery(void) { + size_t live = scr_cyc_live, before = freed; + Obj *r = young_new(); + Obj *a = mature_new(); /* a family that is not young: an array, say */ + Obj *n = young_new(); + r->a = a; /* moved in, never released: a is no candidate */ + store(a, 0, n); + store(n, 0, r); + release(n); + release(r); /* the last outside reference, dropped while r is young */ + for (int i = 0; i < 1024 && freed == before; i++) scr_cyc_collect_scheduled(); + check(freed == before + 3, "dead structure reaching beyond the nursery never collected"); + settle(live, "structure beyond the nursery"); +} + +/* A type whose objects keep surviving stops entering the nursery for a + * while, then is met again. */ +static void check_types_that_do_not_die_young(void) { + enum { KEPT = 5000, PROBE = 64 * 1024 }; + static ScrCycType kept_type = {&obj_trace, &obj_gcfree, 0, 0}; + size_t live = scr_cyc_live; + Obj **kept = malloc(KEPT * sizeof *kept); + for (int i = 0; i < KEPT; i++) kept[i] = young_new_of(&kept_type); + scr_collect_cycles(); /* meets them all: none dies */ + Obj *out = young_new_of(&kept_type); + check(gen_of(out) != SCR_CYC_YOUNG && !buffered(out), "surviving type still enters the nursery"); + release(out); + bool met_again = false; + for (int i = 0; i < 2 * PROBE && !met_again; i++) { + Obj *probe = young_new_of(&kept_type); + met_again = gen_of(probe) == SCR_CYC_YOUNG; + release(probe); + } + check(met_again, "surviving type never met again"); + for (int i = 0; i < KEPT; i++) release(kept[i]); + free(kept); + settle(live, "surviving type left objects behind"); +} + +static void check_publication_takes_young_objects(void) { + size_t live = scr_cyc_live; + Obj *y1 = young_new(), *y2 = young_new(), *y3 = young_new(); + /* scr_pub_mark takes an object whose header reads as buffered, and + * records the answer plus one as its position (0: nothing to restore). */ + if (buffered(y1)) { + check((uint32_t)(scr_cyc_pub_take(y1) + 1) == 0 || small_nursery(), "young take recorded"); + scr_cyc_pub_fill(); + } + check(gen_of(y1) != SCR_CYC_YOUNG && !buffered(y1), "published young object not taken"); + y1->rc = SIZE_MAX; /* published: immortal from here on */ + /* y3 took y1's nursery slot: its death and y2's must still find theirs. */ + release(y3); + release(y2); + settle(live + 1, "nursery inconsistent after a publish took a young object"); + /* A refused publish restores nothing for a young object: it stays out of + * the nursery and every buffer, reachable from the refused graph. */ + Obj *y4 = young_new(); + uint32_t recorded = scr_cyc_pub_take(y4) + 1; + check(recorded == 0, "young take recorded a position"); + scr_cyc_pub_fill(); + check(gen_of(y4) != SCR_CYC_YOUNG && !buffered(y4), "refused publish left the object young"); + release(y4); + settle(live + 1, "refused young object not freed"); +} + +static void check_deep_young_structures(void) { + enum { DEPTH = 200000 }; + static ScrCycType deep_type = {&obj_trace, &obj_gcfree, 0, 0}; + size_t live = scr_cyc_live, before = freed; + /* A long live chain (deep marking), and a long dead chain closing a cycle. */ + Obj *keep = young_new_of(&deep_type), *at = retain(keep); + for (int i = 1; i < DEPTH; i++) { + Obj *n = young_new_of(&deep_type); + store(at, 0, n); + release(at); + at = n; + } + release(at); + Obj *head = young_new_of(&deep_type); + at = retain(head); + for (int i = 1; i < DEPTH; i++) { + Obj *n = young_new_of(&deep_type); + store(at, 0, n); + release(at); + at = n; + } + store(at, 0, head); + release(at); + release(head); + settle(live + DEPTH, "deep young structures"); + check(freed == before + DEPTH, "dead chain not freed exactly"); + release(keep); /* an acyclic chain: freed by reference counting */ + check(freed == before + 2 * DEPTH, "live chain not freed at its release"); + settle(live, "deep young structures left objects behind"); +} + +static void check_collection_points_and_limited_passes(void) { + size_t live = scr_cyc_live, before = freed; + if (small_nursery()) return; + Obj *a = young_new(), *b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + scr_cyc_collect_scheduled(); /* a collection point collects the nursery */ + check(freed == before + 2, "collection point left young garbage"); + a = young_new(); + b = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + void *slots[4] = {0}; + ScrCycSet none = {slots, 3}; + check(scr_cyc_collect_set(&none, SCR_CYC_OLD) >= 2, "limited pass skipped the nursery"); + check(freed == before + 4, "limited pass left young garbage"); + check(scr_cyc_live == live, "limited pass live count"); +} + +static void check_weak_observers_see_young_deaths(void) { + size_t live = scr_cyc_live, before = freed; + disposed = 0; + scr_weak_dispose_hook = count_dispose; + Obj *a = young_new(), *b = young_new(), *c = young_new(); + store(a, 0, b); + store(b, 0, a); + release(a); + release(b); + release(c); /* RC death */ + settle(live, "weak observer case"); + scr_weak_dispose_hook = NULL; + check(freed == before + 3 && disposed == 3, "weak dispose hook missed a young death"); +} + +int main(void) { + check_dead_pair_and_live_ring(); + check_rc_deaths_leave_the_nursery(); + check_dead_set_gives_up_outgoing_edges(); + check_older_holders_keep_young_alive(); + check_trial_deletion_releases_into_the_nursery(); + check_dead_structures_beyond_the_nursery(); + check_types_that_do_not_die_young(); + check_publication_takes_young_objects(); + check_deep_young_structures(); + check_collection_points_and_limited_passes(); + check_weak_observers_see_young_deaths(); + const char *size = getenv("SCR_YOUNG_NURSERY_SIZE"); + printf("young nursery checks passed: size=%s\n", size ? size : "default"); + return 0; +} diff --git a/packages/runtime/test/yin.test.ts b/packages/runtime/test/yin.test.ts new file mode 100644 index 000000000..d36e3f087 --- /dev/null +++ b/packages/runtime/test/yin.test.ts @@ -0,0 +1,80 @@ +import { execFile } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const testDir = import.meta.dirname; + +// The young-in nursery variant of the collector (SCR_YIN), with the RC +// audit's membership and count checks, at nursery sizes from one object +// (a collection at every allocation) to the default. +test("young-in nursery collects young cycles at every nursery size", async () => { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + const bin = join(buildDir, "test_yin"); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-fsanitize=address,undefined", + "-DSCR_CYC_TESTING", + "-DSCR_YIN", + "-DSCR_RC_AUDIT", + "-o", + bin, + join(testDir, "test_yin.c"), + join(testDir, "../src/scr_cycle.c"), + join(testDir, "../src/scr_alloc.c"), + ]); + const baseEnv = { + ...process.env, + ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1", + UBSAN_OPTIONS: "halt_on_error=1", + }; + delete baseEnv.SCR_YOUNG_NURSERY_SIZE; + delete baseEnv.SCR_CYCLE_THRESHOLD; + for (const [size, threshold] of [ + [undefined, undefined], + ["1", undefined], + ["2", undefined], + ["7", undefined], + ["64", "1"], + ["1", "1"], + ] as const) { + const env = { ...baseEnv }; + if (size !== undefined) env.SCR_YOUNG_NURSERY_SIZE = size; + if (threshold !== undefined) env.SCR_CYCLE_THRESHOLD = threshold; + const run = await execFileAsync(bin, [], { env }); + expect(run.stdout).toBe(`young nursery checks passed: size=${size ?? "default"}\n`); + } +}); + +// Trial deletion is unchanged in the young-nursery build: the whole cycle +// suite passes against it (it allocates no young objects). +test("trial deletion checks pass against the young-nursery collector", async () => { + const buildDir = join(testDir, "build"); + await mkdir(buildDir, { recursive: true }); + const bin = join(buildDir, "test_cycle_yin"); + await execFileAsync("clang", [ + "-std=c11", + "-O1", + "-Wall", + "-Wextra", + "-fsanitize=address,undefined", + "-DSCR_CYC_TESTING", + "-DSCR_YIN", + "-o", + bin, + join(testDir, "test_cycle.c"), + join(testDir, "../src/scr_cycle.c"), + join(testDir, "../src/scr_alloc.c"), + ]); + const env = { ...process.env, ASAN_OPTIONS: "detect_leaks=0:halt_on_error=1" }; + delete env.SCR_CYCLE_THRESHOLD; + delete env.SCR_CYCLE_GROWTH_CAP; + const run = await execFileAsync(bin, [], { env }); + expect(run.stdout).toBe("cycle collection checks passed: threshold=256 growth_cap=16\n"); +}); diff --git a/tests/corpus/5981-young-nursery-cycles.ts b/tests/corpus/5981-young-nursery-cycles.ts new file mode 100644 index 000000000..caa59b2a4 --- /dev/null +++ b/tests/corpus/5981-young-nursery-cycles.ts @@ -0,0 +1,178 @@ +// Young cycles of classes and records, dropped at once or kept from every +// kind of outside holder: globals, older objects, arrays, maps, closures, +// weak-map values, error objects, generator and async frames. Constructors +// that throw after linking `this` into a cycle leave a partly built object +// reachable from its peer. Churn between the steps runs collections (with +// the young-in nursery, nursery collections; with SCR_YOUNG_NURSERY_SIZE=1, +// one per allocation). Whatever is still reachable keeps its contents; the +// sanitized lane's RC audit fails if anything leaks or is freed early. + +class Pair { + peer: Pair | null = null; + label: string; + n: number; + constructor(label: string, n: number) { + this.label = label; + this.n = n; + } +} + +interface Cell { + value: number; + next: Cell | null; + owner: Pair | null; +} + +class Fragile { + other: Fragile | null = null; + stage = 0; + name: string; + constructor(name: string, peer: Fragile | null, fail: boolean) { + this.name = name; + this.other = peer; + if (peer) peer.other = this; + this.stage = 1; + if (fail) throw new Error(`${name} failed at stage ${this.stage}`); + this.stage = 2; + } +} + +class Holder extends Error { + held: Pair | null = null; +} + +function pair(label: string, n: number): Pair { + const a = new Pair(`${label}.a`, n); + const b = new Pair(`${label}.b`, n + 1); + a.peer = b; + b.peer = a; + return a; +} + +function ring(size: number, base: number): Cell { + const first: Cell = { value: base, next: null, owner: null }; + let last = first; + for (let i = 1; i < size; i++) { + const cell: Cell = { value: base + i, next: null, owner: null }; + last.next = cell; + last = cell; + } + last.next = first; + return first; +} + +function ringSum(start: Cell): number { + let total = start.value; + for (let c = start.next; c !== null && c !== start; c = c.next) total += c.value; + return total; +} + +function churn(rounds: number): number { + let acc = 0; + for (let i = 0; i < rounds; i++) { + acc = (acc + pair("t", i).peer!.n) % 1000003; + acc = (acc + ringSum(ring(5, i))) % 1000003; + } + return acc; +} + +function show(p: Pair | null): string { + return p === null ? "null" : `${p.label}:${p.n}<->${p.peer?.label ?? "none"}`; +} + +// Dropped at once, in bulk. +console.log("churn", churn(20000)); + +// Kept from a global, an older object, an array, a map and a closure. +const kept = pair("global", 1); +const old = new Pair("old", 0); +console.log("churn", churn(5000)); +old.peer = pair("from-old", 2); +const list: Pair[] = []; +const byName = new Map(); +for (let i = 0; i < 50; i++) { + list.push(pair(`list${i}`, i)); + byName.set(`map${i}`, pair(`map${i}`, i)); +} +const captured = pair("closure", 3); +const read = (): string => show(captured.peer); +const cells: Cell[] = []; +for (let i = 0; i < 20; i++) { + const c = ring(4, i * 10); + c.owner = pair(`cell${i}`, i); + cells.push(c); +} +console.log("churn", churn(30000)); +console.log(show(kept), show(kept.peer)); +console.log(show(old.peer), show(old.peer!.peer)); +console.log(show(list[17]!), show(byName.get("map33")!.peer)); +console.log(read()); +console.log(ringSum(cells[7]!), show(cells[7]!.owner), ringSum(cells[19]!.next!)); + +// A weak map whose value refers back to its young key keeps the key while +// the key is held; dropping the key leaves the pair to the collector. +const weak = new WeakMap(); +const key = pair("key", 7); +weak.set(key, key.peer!); +for (let i = 0; i < 200; i++) { + const k = pair(`weak${i}`, i); + weak.set(k, k.peer!); +} +console.log("churn", churn(20000)); +console.log(weak.has(key), show(weak.get(key) ?? null)); + +// Error objects (runtime-rooted classes) holding young cycles. +const errors: Holder[] = []; +for (let i = 0; i < 10; i++) { + const e = new Holder(`holder ${i}`); + e.held = pair(`err${i}`, i); + errors.push(e); +} +console.log("churn", churn(10000)); +console.log(errors[4]!.message, show(errors[4]!.held)); + +// Constructors that throw after linking into a cycle. +const survivors: Fragile[] = []; +for (let i = 0; i < 100; i++) { + const peer = new Fragile(`peer${i}`, null, false); + try { + new Fragile(`victim${i}`, peer, i % 3 === 0); + } catch (e) { + if (i < 6) console.log((e as Error).message); + } + if (i % 10 === 0) survivors.push(peer); +} +console.log("churn", churn(10000)); +for (const s of survivors.slice(0, 4)) + console.log(s.name, s.stage, s.other?.name, s.other?.stage, s.other?.other === s); + +// Generator frames hold young objects across collections. +function* pairs(count: number): Generator { + for (let i = 0; i < count; i++) { + const p = pair(`gen${i}`, i); + churn(500); + yield p; + } +} +let genTotal = 0; +for (const p of pairs(30)) genTotal += p.peer!.n; +console.log("generator", genTotal); + +// Async frames hold young objects across awaits. +async function hold(n: number): Promise { + const a = pair(`async${n}`, n); + const cell = ring(3, n); + cell.owner = a; + await null; + churn(3000); + await new Promise((resolve) => setTimeout(resolve, 1)); + churn(3000); + return `${show(a)} ${show(cell.owner)} ${ringSum(cell)}`; +} +async function main(): Promise { + const pending: Promise[] = [hold(1), hold(2), hold(3)]; + for (const p of pending) console.log(await p); + console.log("churn", churn(10000)); + console.log(show(kept), show(list[49]!), read()); +} +void main(); diff --git a/tests/corpus/5982-young-nursery-workers.ts b/tests/corpus/5982-young-nursery-workers.ts new file mode 100644 index 000000000..64c3b30f5 --- /dev/null +++ b/tests/corpus/5982-young-nursery-workers.ts @@ -0,0 +1,81 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; + +// Every thread builds and drops young cycles of its own while it publishes +// graphs of freshly built objects and posts them. Publishing takes young +// objects out of the thread's nursery before they become shared, so the +// receiving thread reads them intact while both threads keep collecting. +// Receivers publish() what they receive (a no-op in scriptc) and read only +// data, so Node.js prints the same. + +class Node { + parent: Node | null = null; + next: Node | null = null; + children: Node[] = []; + label: string; + constructor(label: string) { + this.label = label; + } +} + +function graph(prefix: string, width: number): Node { + const root = new Node(`${prefix}root`); + let last: Node | null = null; + for (let i = 0; i < width; i++) { + const child = new Node(`${prefix}${i}`); + child.parent = root; + if (last !== null) last.next = child; + last = child; + root.children.push(child); + } + if (last !== null) last.next = root.children[0]!; + return root; +} + +function churn(rounds: number): number { + let acc = 0; + for (let i = 0; i < rounds; i++) { + const a = new Node(`a${i}`); + const b = new Node(`b${i}`); + a.next = b; + b.next = a; + acc = (acc + a.next.label.length + b.next.label.length) % 1000003; + } + return acc; +} + +function describe(root: Node): string { + let out = root.label; + for (const c of root.children) out += ` ${c.label}^${c.parent?.label}>${c.next?.label}`; + return out; +} + +if (isMainThread) { + const replies: string[] = []; + let pending = 3; + for (let w = 0; w < 3; w++) { + const shared = publish(graph(`m${w}.`, 3)); + const worker = new Worker(new URL(import.meta.url), { workerData: { id: w, shared } }); + worker.on("message", (message: unknown) => { + const received = message as { id: number; graph: Node; churned: number }; + publish(received.graph); + replies[w] = `worker ${received.id}: ${describe(received.graph)} churn=${received.churned}`; + }); + worker.on("exit", () => { + if (--pending === 0) { + for (const line of replies) console.log(line); + console.log(`main churn ${churn(20000)}`); + } + }); + } + console.log(`main churn ${churn(20000)}`); +} else { + const { id, shared } = workerData as { id: number; shared: Node }; + publish(shared); + const churned = churn(30000); + const fresh = graph(`w${id}.`, 4); + fresh.children[1]!.children.push(new Node(`w${id}.leaf`)); + publish(fresh); + churn(10000); + parentPort!.postMessage({ id, graph: fresh, churned: (churned + describe(shared).length) % 1000003 }); +} From 526415a91ceaffd0d28fd2cdfbf680b803a1d8e1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 14:43:40 -0500 Subject: [PATCH 04/67] Give matchAll rows and exec results their index, input and groups Match results record their metadata lazily. The results of one exec or matchAll call share a source (the subject and the regex), and each result keeps its start index. index, input and a null-prototype groups object are built when a result is converted to a dynamic value; .index and .input on a match slice read the source directly. matchAll rows are allocated inline, like exec results. A drain whose rows are read only through their elements, .length, the .groups projection and the companion index uses matchAllCaptures or matchAllCapturesInto, whose rows carry no metadata. --- .../compiler/src/backend/llvm/expr-records.ts | 12 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/backend/llvm/reference-effects.ts | 1 + .../src/backend/llvm/reference-writes.ts | 1 + packages/compiler/src/backend/may-throw.ts | 4 +- .../lowering/containers/match-row-uses.ts | 299 ++++++++++++++++++ .../lowering/containers/string-and-regexp.ts | 3 +- .../src/frontend/lowering/lower-exprs.ts | 12 +- .../src/frontend/lowering/lower-stmts.ts | 12 +- .../compiler/src/ir/builtin-signatures.ts | 2 + .../compiler/src/ir/intrinsic-signatures.ts | 6 + packages/compiler/src/ir/ir.ts | 10 +- .../compiler/test/match-row-emission.test.ts | 69 ++++ .../tests/corpus/6010-match-metadata.ts.json | 6 + .../corpus/6011-match-metadata-js.js.json | 6 + packages/runtime/src/scr_array.c | 4 + packages/runtime/src/scr_array.h | 14 + packages/runtime/src/scr_dyn.h | 3 + packages/runtime/src/scr_json.c | 8 + packages/runtime/src/scr_publish.c | 2 +- packages/runtime/src/scr_regex.c | 170 +++++++++- packages/runtime/src/scr_regex.h | 14 +- tests/corpus/6010-match-metadata.ts | 40 +++ tests/corpus/6011-match-metadata-js.js | 31 ++ tests/harness/corpus-costs.json | 4 + 25 files changed, 705 insertions(+), 29 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/containers/match-row-uses.ts create mode 100644 packages/compiler/test/match-row-emission.test.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json create mode 100644 tests/corpus/6010-match-metadata.ts create mode 100644 tests/corpus/6011-match-metadata-js.js diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 3318ae774..0cbfe15dd 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -147,7 +147,9 @@ function directRegexOperandMethod(method: string): boolean { case "match": case "search": case "matchAll": + case "matchAllCaptures": case "matchAllInto": + case "matchAllCapturesInto": case "replace": case "replaceAll": case "split": @@ -192,7 +194,7 @@ export function emitRegexIntrinsic( } else { values = preservesRegexInputs(method) ? emitBorrowedInputs(host, inputs) - : method === "matchAllInto" + : method === "matchAllInto" || method === "matchAllCapturesInto" ? inputs.map((a) => emitBorrowedInput(host, a)) : inputs.map((a) => host.emitExpr(a)); } @@ -216,6 +218,9 @@ export function emitRegexIntrinsic( // Every match drained eagerly into a fresh +1 string[][]; throws // Node's TypeError on a non-global regex (catchable). return fallible("scr_regex_match_all", `ptr ${r.name}, ptr ${args[0]!.name}`); + case "matchAllCaptures": + // matchAll whose rows carry no index, input or groups properties. + return fallible("scr_regex_match_all_captures", `ptr ${r.name}, ptr ${args[0]!.name}`); case "matchAllInto": // matchAll's companion-index form: args[1] (a number[]) also // receives each match's UTF-16 start index. @@ -223,6 +228,11 @@ export function emitRegexIntrinsic( "scr_regex_match_all_into", `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, ); + case "matchAllCapturesInto": + return fallible( + "scr_regex_match_all_captures_into", + `ptr ${r.name}, ptr ${args[0]!.name}, ptr ${args[1]!.name}`, + ); case "replaceAll": return fallible( "scr_regex_replace_all", diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02bf37192..1334642a2 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -1394,6 +1394,7 @@ export const LIB_FN_SYMS: Record = { "regex.groupEquals": "scr_regex_group_eq", "regex.groupNumber": "scr_regex_group_number", "regex.groupParseInt": "scr_regex_group_parse_int", + "regex.resultProp": "scr_regex_result_prop", // queueMicrotask's checked-dynamic form (borrowed dyn; a non-function // throws synchronously), the minted setImmediate value, and the // timers/promises immediate — all mark the loop live. diff --git a/packages/compiler/src/backend/llvm/reference-effects.ts b/packages/compiler/src/backend/llvm/reference-effects.ts index 88875fce4..e7b044187 100644 --- a/packages/compiler/src/backend/llvm/reference-effects.ts +++ b/packages/compiler/src/backend/llvm/reference-effects.ts @@ -29,6 +29,7 @@ export function preservesRegexInputs(method: string): boolean { case "match": case "search": case "matchAll": + case "matchAllCaptures": case "replace": case "replaceAll": case "split": diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts index 2371147a9..dcd6058d8 100644 --- a/packages/compiler/src/backend/llvm/reference-writes.ts +++ b/packages/compiler/src/backend/llvm/reference-writes.ts @@ -104,6 +104,7 @@ const INERT_LIB_CALLS = new Set([ "regex.groupEquals", "regex.groupNumber", "regex.groupParseInt", + "regex.resultProp", "process.cwd", "process.argv", "process.execArgv", diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index ac45129f0..2881fcbeb 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -297,7 +297,9 @@ export function computeMayThrow( rec.method === "replaceAll" || rec.method === "split" || rec.method === "matchAll" || - rec.method === "matchAllInto" + rec.method === "matchAllCaptures" || + rec.method === "matchAllInto" || + rec.method === "matchAllCapturesInto" ) { f.throws = true; } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts new file mode 100644 index 000000000..387ff81d3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -0,0 +1,299 @@ +import * as ts from "../../ts7/adapter.js"; +import type { Lowerer } from "../lowerer.js"; +import { identifiersNamed } from "../identifier-uses.js"; +import { bindingNeverReassigned } from "../binding-analysis.js"; +import { matchCallbackParamGroupsOf, matchResultNamedGroupsOf } from "../lower-exprs.js"; + +/** Whether the rows a matchAll drain produces are read only through their + * elements: element reads and `in` tests with number keys, `.length`, the compile-time + * `.groups` projection, `{ groups }` destructuring and, on a for-of binding + * over the drain itself (`companion`), the companion-index `.index`. Rows + * reach these reads through for-of loops, spreads, `Array.from`, stored + * const bindings, `filter`/`slice`/`toReversed` copies, `find`/`findLast`/ + * `at` picks and inline callbacks of the array methods below. No such read + * can observe a row's `index`, `input` or `groups` properties, so the + * drain leaves them out. False for any other use. */ +export function matchAllRowsReadByElements( + lowerer: Lowerer, + drain: ts.Expression, + companion: boolean, +): boolean { + const parent = outerValue(drain).parent; + if ( + ts.isCallExpression(parent) && + parent.arguments[0] === outerValue(drain) && + ts.isPropertyAccessExpression(parent.expression) && + parent.expression.name.text === "from" && + lowerer.isStdlibGlobal(parent.expression.expression, "Array") + ) { + const mapper = parent.arguments[1]; + if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0); + return parent.arguments.length === 2 && callbackReadsByElements(lowerer, mapper, 2); + } + if (ts.isForOfStatement(parent) && parent.expression === outerValue(drain)) + return forOfReadsByElements(lowerer, parent, companion); + return rowsReadByElements(lowerer, drain, 0); +} + +/** matchAllRowsReadByElements for a stored drain, `const rows = + * s.matchAll(re)`: a const-binding for-of loop over `rows` in the same + * function walks the companion indices, so its binding may also read + * `.index`. */ +export function storedMatchAllRowsReadByElements( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): boolean { + const scope = enclosingFunction(decl); + return constBindingReadsByElements(lowerer, decl, (use) => { + const parent = outerValue(use).parent; + if ( + ts.isForOfStatement(parent) && + parent.expression === outerValue(use) && + ts.isVariableDeclarationList(parent.initializer) && + (parent.initializer.flags & ts.NodeFlags.Const) !== 0 && + enclosingFunction(parent) === scope + ) + return forOfReadsByElements(lowerer, parent, true); + return rowsReadByElements(lowerer, use, 1); + }); +} + +function enclosingFunction(node: ts.Node): ts.Node | undefined { + let at = node.parent; + while (at !== undefined && !ts.isFunctionLike(at)) at = at.parent; + return at; +} + +/** Array methods whose first argument is a callback over the rows. */ +const ROW_CALLBACK_METHODS = new Set([ + "map", + "forEach", + "filter", + "some", + "every", + "find", + "findLast", + "findIndex", + "findLastIndex", + "flatMap", +]); + +/** Array methods whose result holds rows of the receiver. */ +const ROW_SUBSET_METHODS = new Set(["filter", "slice", "toReversed"]); + +/** Array methods that return one row of the receiver. */ +const ROW_PICK_METHODS = new Set(["find", "findLast", "at"]); + +/** Bounds the walk through stored bindings and copies. */ +const MAX_DEPTH = 8; + +function outerValue(node: ts.Node): ts.Node { + let ref = node; + while (ts.isParenthesizedExpression(ref.parent) || ts.isNonNullExpression(ref.parent)) + ref = ref.parent; + return ref; +} + +/** `node` evaluates to an array of rows. */ +function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean { + if (depth > MAX_DEPTH) return false; + const ref = outerValue(node); + const parent = ref.parent; + if (ts.isSpreadElement(parent)) { + const literal = parent.parent; + return ( + ts.isArrayLiteralExpression(literal) && + literal.elements.length === 1 && + rowsReadByElements(lowerer, literal, depth + 1) + ); + } + if (ts.isForOfStatement(parent) && parent.expression === ref) + return forOfReadsByElements(lowerer, parent, false); + if (ts.isVariableDeclaration(parent) && parent.initializer === ref) + return constBindingReadsByElements(lowerer, parent, (use) => + rowsReadByElements(lowerer, use, depth + 1), + ); + if (ts.isElementAccessExpression(parent) && parent.expression === ref) + return numberKeyRead(lowerer, parent) && rowReadByElements(lowerer, parent, depth + 1); + if (!ts.isPropertyAccessExpression(parent) || parent.expression !== ref) return false; + if (parent.questionDotToken !== undefined || !lowerer.isStdlibMember(parent)) return false; + const method = parent.name.text; + const call = parent.parent; + if (!ts.isCallExpression(call) || call.expression !== parent) + return method === "length" && isValueRead(parent); + if (ROW_CALLBACK_METHODS.has(method)) { + if (call.arguments.length !== 1 || !callbackReadsByElements(lowerer, call.arguments[0]!, 2)) + return false; + } else if (ROW_SUBSET_METHODS.has(method) || ROW_PICK_METHODS.has(method)) { + if (call.arguments.some((a) => ts.isSpreadElement(a))) return false; + } else { + return false; + } + if (ROW_SUBSET_METHODS.has(method)) return rowsReadByElements(lowerer, call, depth + 1); + if (ROW_PICK_METHODS.has(method)) return rowReadByElements(lowerer, call, depth + 1); + return true; +} + +/** `node` evaluates to one row. */ +function rowReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boolean { + if (depth > MAX_DEPTH) return false; + const ref = outerValue(node); + const parent = ref.parent; + if (ts.isExpressionStatement(parent)) return true; + if (ts.isElementAccessExpression(parent) && parent.expression === ref) + return numberKeyRead(lowerer, parent); + // `1 in m`: a number key tests an element. + if ( + ts.isBinaryExpression(parent) && + parent.operatorToken.kind === ts.SyntaxKind.InKeyword && + parent.right === ref + ) + return lowerer.mapTypeOf(lowerer.typeOf(parent.left))?.kind === "f64"; + if (ts.isPropertyAccessExpression(parent) && parent.expression === ref) { + if (parent.questionDotToken !== undefined || !isValueRead(parent)) return false; + if (parent.name.text === "length") return true; + return parent.name.text === "groups" && groupsProjected(lowerer, ref as ts.Expression); + } + if (ts.isVariableDeclaration(parent) && parent.initializer === ref) { + if (ts.isObjectBindingPattern(parent.name)) + return groupsPattern(parent.name) && groupsProjected(lowerer, ref as ts.Expression); + return constBindingReadsByElements(lowerer, parent, (use) => + rowReadByElements(lowerer, use, depth + 1), + ); + } + return false; +} + +/** A for-of loop over rows: its binding is read by elements, or by its + * companion `.index` when the loop walks the drain itself. */ +function forOfReadsByElements( + lowerer: Lowerer, + loop: ts.ForOfStatement, + companion: boolean, +): boolean { + const list = loop.initializer; + if (!ts.isVariableDeclarationList(list) || list.declarations.length !== 1) return false; + const decl = list.declarations[0]!; + if (!ts.isIdentifier(decl.name)) return false; + const symbol = lowerer.resolveValueSymbol(decl.name); + if (!symbol || !bindingNeverReassigned(lowerer, symbol, decl)) return false; + return usesOf(lowerer, decl.name, symbol).every((use) => { + const parent = outerValue(use).parent; + if ( + companion && + ts.isPropertyAccessExpression(parent) && + parent.name.text === "index" && + parent.questionDotToken === undefined && + isValueRead(parent) + ) + return true; + return rowReadByElements(lowerer, use, 1); + }); +} + +/** An inline callback whose first parameter (a row) is read by elements. + * The callback may name at most `maxParams` parameters, which keeps the + * array argument (the rows themselves) out of reach. */ +function callbackReadsByElements( + lowerer: Lowerer, + node: ts.Expression, + maxParams: number, +): boolean { + let fn: ts.Node = node; + while (ts.isParenthesizedExpression(fn)) fn = fn.expression; + if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; + if (fn.parameters.length > maxParams) return false; + const param = fn.parameters[0]; + if (param === undefined) return true; + if (param.dotDotDotToken !== undefined || param.initializer !== undefined) return false; + if (ts.isObjectBindingPattern(param.name)) + return groupsPattern(param.name) && groupsProjectedParam(lowerer, param); + if (!ts.isIdentifier(param.name)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (!symbol || !bindingNeverReassigned(lowerer, symbol, param)) return false; + return usesOf(lowerer, param.name, symbol).every((use) => rowReadByElements(lowerer, use, 1)); +} + +/** Every use of a non-exported const binding satisfies `read`. */ +function constBindingReadsByElements( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + read: (use: ts.Identifier) => boolean, +): boolean { + if ( + !ts.isIdentifier(decl.name) || + decl.type !== undefined || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 + ) + return false; + const statement = decl.parent.parent; + if ( + ts.isVariableStatement(statement) && + statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + ) + return false; + const symbol = lowerer.resolveValueSymbol(decl.name); + if (!symbol) return false; + return usesOf(lowerer, decl.name, symbol).every(read); +} + +function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] { + return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter( + (use) => use !== name && lowerer.resolveValueSymbol(use) === symbol, + ); +} + +/** `rows[i]` or `rows?.[i]` with a number key, read and not written. */ +function numberKeyRead(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { + if (!isValueRead(access)) return false; + const key = lowerer.mapTypeOf(lowerer.typeOf(access.argumentExpression)); + return key?.kind === "f64"; +} + +/** `m.groups` lowers to the compile-time projection over the row's + * elements (lowerMatchGroupsRead). */ +function groupsProjected(lowerer: Lowerer, row: ts.Expression): boolean { + const groups = matchResultNamedGroupsOf(lowerer, row); + return groups !== null && groups.length > 0; +} + +/** `({ groups }) => ...` lowers through the same projection. */ +function groupsProjectedParam(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean { + const groups = matchCallbackParamGroupsOf(lowerer, param); + return groups !== null && groups.length > 0; +} + +/** An object pattern that binds only `groups`. */ +function groupsPattern(pattern: ts.ObjectBindingPattern): boolean { + return ( + pattern.elements.length > 0 && + pattern.elements.every((el) => { + if (el.dotDotDotToken !== undefined) return false; + const prop = el.propertyName; + if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; + return (ts.isIdentifier(prop) || ts.isStringLiteral(prop)) && prop.text === "groups"; + }) + ); +} + +/** A read whose value is only consumed: not an assignment, update or + * delete target. */ +function isValueRead(access: ts.Expression): boolean { + const ref = outerValue(access); + const parent = ref.parent; + if ( + ts.isBinaryExpression(parent) && + parent.left === ref && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + return !( + (ts.isPrefixUnaryExpression(parent) && + (parent.operator === ts.SyntaxKind.PlusPlusToken || + parent.operator === ts.SyntaxKind.MinusMinusToken)) || + ts.isPostfixUnaryExpression(parent) || + ts.isDeleteExpression(parent) + ); +} 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 361441636..dd70eb197 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { @@ -869,7 +870,7 @@ export function lowerRegexMethodCall( const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { kind: "regexIntrinsic", - method: "matchAll", + method: matchAllRowsReadByElements(lowerer, call, false) ? "matchAllCaptures" : "matchAll", receiver, args: [re], type: arrayOf(arrayOf(STRING)), diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..48d9d68fe 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2053,13 +2053,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile())) ) { const recv = represented; - const read: IrExpr = { - kind: "dynKeyGet", - value: lowerer.coerceToExpected(recv, DYN), - key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, - type: DYN, - loc, - }; + const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc }; + // A plain slice answers from its match source without a dynamic copy. + const read: IrExpr = typeEquals(recv.type, arrayOf(STRING)) + ? { kind: "libCall", fn: "regex.resultProp", args: [recv, key], type: DYN, loc } + : { kind: "dynKeyGet", value: lowerer.coerceToExpected(recv, DYN), key, type: DYN, loc }; return lowerer.maybeNarrow(read, expr); } } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0f1b606ef..99e768e32 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,7 @@ +import { + matchAllRowsReadByElements, + storedMatchAllRowsReadByElements, +} from "./containers/match-row-uses.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -1746,7 +1750,9 @@ function lowerMatchAllDrainDecl( localId: local.id, init: { kind: "regexIntrinsic", - method: "matchAllInto", + method: storedMatchAllRowsReadByElements(lowerer, decl) + ? "matchAllCapturesInto" + : "matchAllInto", receiver, args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }], type: rowsT, @@ -9289,7 +9295,9 @@ function lowerForOfMatchAll( }); rowsInit = { kind: "regexIntrinsic", - method: "matchAllInto", + method: matchAllRowsReadByElements(lowerer, call, true) + ? "matchAllCapturesInto" + : "matchAllInto", receiver, args: [re, { kind: "varRef", localId: idxs.id, type: idxsT, loc }], type: rowsT, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 08c493ae4..5657546de 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3377,6 +3377,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "regex.groupEquals": { argTypes: [arrayOf(STRING), F64, STRING], result: BOOL }, "regex.groupNumber": { argTypes: [arrayOf(STRING), F64], result: F64 }, "regex.groupParseInt": { argTypes: [arrayOf(STRING), F64, F64], result: F64 }, + // `.index` / `.input` (args[1]) of a match result. Borrowed; never throws. + "regex.resultProp": { argTypes: [arrayOf(STRING), STRING], result: DYN }, // node:events EventEmitter: receivers are emitter-hierarchy objects and // the chaining forms (on/off/removeAll/setMax) return the receiver's // own class — program-dependent object types, checked in the libCall diff --git a/packages/compiler/src/ir/intrinsic-signatures.ts b/packages/compiler/src/ir/intrinsic-signatures.ts index 3c224f8bc..eda569883 100644 --- a/packages/compiler/src/ir/intrinsic-signatures.ts +++ b/packages/compiler/src/ir/intrinsic-signatures.ts @@ -53,11 +53,17 @@ export const REGEX_INTRINSIC_SIGS: Record< matchGroups: { receiver: STRING, argTypes: [REGEX, F64, F64], result: VOID }, lastIndex: { receiver: REGEX, argTypes: [], result: F64 }, matchAll: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) }, + matchAllCaptures: { receiver: STRING, argTypes: [REGEX], result: arrayOf(arrayOf(STRING)) }, matchAllInto: { receiver: STRING, argTypes: [REGEX, arrayOf(F64)], result: arrayOf(arrayOf(STRING)), }, + matchAllCapturesInto: { + receiver: STRING, + argTypes: [REGEX, arrayOf(F64)], + result: arrayOf(arrayOf(STRING)), + }, search: { receiver: STRING, argTypes: [REGEX], result: F64 }, source: { receiver: REGEX, argTypes: [], result: STRING }, flags: { receiver: REGEX, argTypes: [], result: STRING }, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..74f5197ac 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -2365,13 +2365,21 @@ export type IrRegexIntrinsicMethod = * unobservable across the lowered surface (strings are immutable; the * spec clones the regex at the call, so lastIndex games can't reach the * drain either). Non-global regexes THROW Node's exact TypeError - * (catchable — replaceAll's stance). */ + * (catchable — replaceAll's stance). Each row carries its index, input + * and groups properties. */ | "matchAll" + /** matchAll whose rows are read only through their elements + * (matchAllRowsReadByElements): the rows carry no index, input or + * groups properties. Same result and throw contract as matchAll. */ + | "matchAllCaptures" /** matchAll's companion-index form (the for-of-over-matchAll desugar): * args[1] is a number[] the drain ALSO fills with each match's UTF-16 * start index — the row's `.index`, always present (every drained row * matched). Same result and throw contract as matchAll. */ | "matchAllInto" + /** matchAllInto whose rows are read only through their elements and the + * companion index: the rows carry no index, input or groups properties. */ + | "matchAllCapturesInto" /** `s.search(re)` — the first match's UTF-16 index, or -1. Symbol.search * neither reads nor writes lastIndex (a fresh exec from position 0), so * no g/y fence applies: /g is irrelevant and /y anchors at 0 — exactly diff --git a/packages/compiler/test/match-row-emission.test.ts b/packages/compiler/test/match-row-emission.test.ts new file mode 100644 index 000000000..39e07bc1c --- /dev/null +++ b/packages/compiler/test/match-row-emission.test.ts @@ -0,0 +1,69 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-match-rows-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const calls = (llvm: string, symbol: string): number => + llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)).length; + +test("rows read only through their elements carry no match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val, 1 in m); +console.log([...text.matchAll(re)].map((m) => m[2]).join(",")); +console.log([...text.matchAll(re)].filter((m) => m.length > 2).find((m) => m[1] === "k2")?.[2]); +console.log(Array.from(text.matchAll(re), ({ groups }) => groups!.key).join(",")); +const rows = [...text.matchAll(re)]; +for (const row of rows.slice(1)) console.log(row[0]); +`); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(4); + expect(calls(llvm, "scr_regex_match_all")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(0); +}); + +test("rows whose properties are read or that escape keep match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.input.length); +console.log([...text.matchAll(re)].map((m) => m.index).join(",")); +function first(m: RegExpExecArray): string { + return m[1]!; +} +console.log([...text.matchAll(re)].map(first).join(",")); +console.log([...text.matchAll(re)].map((m) => m)[0]![1]); +`); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all")).toBe(3); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(0); +}); + +test(".index and .input on a match slice read its match source", async () => { + const llvm = await emit(` +const rows = [..."a1b2".matchAll(/[a-z](\\d)/g)]; +console.log(rows[1]!.index, rows[0]!.input); +const m = /b/.exec("ab")!; +console.log(m.index); +`); + expect(calls(llvm, "scr_regex_result_prop")).toBe(3); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json new file mode 100644 index 000000000..d2b199768 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6010-match-metadata.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6010-match-metadata.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json new file mode 100644 index 000000000..437b28911 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6011-match-metadata-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6011-match-metadata-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index ef64be56d..fa494d1e5 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -40,6 +40,8 @@ static void scr_arr_oom(void) { /* Readers of the out-of-line storage see an all-empty one until the first * sparse entry, noncanonical property or metadata allocates it. */ static const ScrArrExt scr_arr_no_ext; + +SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks = NULL; #define SCR_AX(a) ((const ScrArrExt *)((a)->ext ? (a)->ext : &scr_arr_no_ext)) ScrArrExt *scr_arr_ext(ScrArr *a) { @@ -1068,6 +1070,7 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { * arrays have none. */ static void scr_arr_ext_free(ScrArr *a) { ScrArrExt *x = a->ext; + if (x->match_source) scr_arr_match_hooks->release(x->match_source); scr_arr_sparse_free(x->sparse); for (size_t i = 0; i < x->prop_len; i++) free(x->props[i].key); free(x->props); @@ -4423,6 +4426,7 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { + if (SCR_AX(a)->match_source && scr_arr_match_hooks->copy_metadata(a, target)) return; if (!SCR_AX(a)->metadata) return; for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) { ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i]; diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index d02c2097e..616aa4c75 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -106,6 +106,11 @@ typedef struct ScrArrExt { size_t prop_len; size_t prop_cap; struct ScrDyn *metadata; /* heterogeneous named properties of match results */ + /* A match result's metadata before its first use: the match's shared + * subject and regex (scr_regex.c builds `metadata` from them) and its + * UTF-16 start index. */ + struct ScrRegexMatchSource *match_source; + double match_index; } ScrArrExt; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like @@ -207,6 +212,15 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); +/* The match-source operations, installed by scr_regex.c when it records a + * pending match source (ScrArrExt.match_source); this unit links without + * the regex runtime. copy_metadata writes the pending metadata into the + * dynamic copy or materializes it, answering whether it wrote. */ +typedef struct ScrArrMatchHooks { + void (*release)(struct ScrRegexMatchSource *src); + bool (*copy_metadata)(ScrArr *a, struct ScrDyn *target); +} ScrArrMatchHooks; +extern SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks; /* The array's out-of-line storage, allocated (zeroed) on first use. */ ScrArrExt *scr_arr_ext(ScrArr *a); /* Moves inline dense slots (kept in the header block of a small array) to diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 667ceced0..8ebe11265 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -862,6 +862,9 @@ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity * non-object kinds throw Node's catchable TypeErrors (strict-mode * wording). All three operands BORROWED (the value is retained in). */ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value); +/* A named (non-index) property of an extensible, unfrozen ARR, stored as + * scr_dyn_key_set stores it. Takes ownership of value. */ +void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value); void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value); void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b23e78241..1eca0c9ec 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -7059,6 +7059,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } +void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value) { + if (!arr->v.arr.properties) { + scr_dyn_region_store(arr, NULL); /* a table that later holds anything */ + arr->v.arr.properties = scr_dyn_new_obj(); + } + scr_dyn_obj_set(arr->v.arr.properties, key, key_len, value); +} + void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (scr_dyn_class_reflection_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 9dedc6844..a974e82ff 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -188,7 +188,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; - if (a->ext && (a->ext->props || a->ext->metadata)) { + if (a->ext && (a->ext->props || a->ext->metadata || a->ext->match_source)) { scr_pub_refuse(ctx, "Cannot publish an array with non-index properties"); return; } diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index b24261a4d..40e843cac 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -610,6 +610,124 @@ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) { return scr_str_new((const char *)input->data + start, (size_t)(end - start)); } +/* ── match metadata ───────────────────────────────────────────────────── */ + +/* Results of one exec or matchAll call share their subject and regex. + * Each result records the source and its start index; the metadata + * object (index, input, groups) is built when a result is first + * converted to a dynamic value, so results read only through their + * elements never allocate it. */ +typedef struct ScrRegexMatchSource { + size_t rc; + ScrStr *input; + ScrRegex *re; + ScrDyn *input_value; /* `input` as a dynamic value, built on first use */ +} ScrRegexMatchSource; + +static void scr_regex_materialize_metadata(ScrArr *a); +static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target); + +static ScrRegexMatchSource *scr_regex_source_new(ScrStr *s, ScrRegex *re) { + ScrRegexMatchSource *src = malloc(sizeof *src); + if (!src) scr_regex_oom(); + src->rc = 1; + src->input = scr_str_retain(s); + src->re = scr_regex_retain(re); + src->input_value = NULL; + return src; +} + +static void scr_regex_source_release(ScrRegexMatchSource *src) { + if (!src || --src->rc != 0) return; + scr_str_release(src->input); + scr_regex_release(src->re); + scr_dyn_release(src->input_value); + free(src); +} + +/* Records the match at UTF-16 `index` of `src` as the row's pending + * metadata. */ +static void scr_regex_defer_metadata(ScrArr *row, ScrRegexMatchSource *src, int index) { + static const ScrArrMatchHooks hooks = { scr_regex_source_release, scr_regex_copy_pending_metadata }; + scr_arr_match_hooks = &hooks; + ScrArrExt *x = scr_arr_ext(row); + src->rc++; + x->match_source = src; + x->match_index = (double)index; +} + +/* The subject as a dynamic value, shared by the source's results. +1. */ +static ScrDyn *scr_regex_source_input(ScrRegexMatchSource *src) { + if (!src->input_value) src->input_value = scr_dyn_new_str(src->input); + return scr_dyn_retain(src->input_value); +} + +ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { + ScrArrExt *x = a->ext; + if (x && x->match_source && !x->metadata) { + if (key->len == 5 && memcmp(key->data, "index", 5) == 0) + return scr_dyn_new_num(x->match_index); + if (key->len == 5 && memcmp(key->data, "input", 5) == 0) + return scr_regex_source_input(x->match_source); + scr_regex_materialize_metadata(a); + } + ScrDyn *value = x && x->metadata ? scr_dyn_obj_get(x->metadata, key->data, key->len) : NULL; + return scr_dyn_retain(value ? value : scr_dyn_undefined()); +} + +/* groups is a null-prototype object mapping each group name to its first + * participating capture (the row's own string, shared) or undefined, and + * undefined itself for a pattern without named groups. The row holds each + * capture as a string or a present undefined. +1. */ +static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { + const char *groupnames = lre_get_groupnames(bc); + if (!groupnames) return scr_dyn_retain(scr_dyn_undefined()); + int capture_count = lre_get_capture_count(bc); + ScrDyn *groups = scr_dyn_new_obj_null_proto(); + const char *name = groupnames; + for (int k = 1; k < capture_count; k++) { + size_t length = strlen(name); + bool matched = (size_t)k < a->len && (size_t)k < a->cap && a->present[k] == SCR_ARR_VALUE; + /* ES2025 duplicates sit in distinct alternatives: at most one + * participates, and it wins over an earlier undefined. */ + if (length && (matched || !scr_dyn_obj_get(groups, name, length))) + scr_dyn_obj_set(groups, name, length, + matched ? scr_dyn_new_str((ScrStr *)(uintptr_t)a->data[k]) + : scr_dyn_retain(scr_dyn_undefined())); + name += length + LRE_GROUP_NAME_TRAILER_LEN; + } + return groups; +} + +static void scr_regex_materialize_metadata(ScrArr *a) { + ScrArrExt *x = scr_arr_ext(a); + ScrRegexMatchSource *src = x->match_source; + if (!src) return; + x->match_source = NULL; + if (!x->metadata) { + ScrDyn *props = scr_dyn_new_obj(); + x->metadata = props; + scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); + scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + } + scr_regex_source_release(src); +} + +static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target) { + ScrArrExt *x = scr_arr_ext(a); + ScrRegexMatchSource *src = x->match_source; + if (target->kind != SCR_DYN_ARR || target->v.arr.properties || target->non_extensible || + target->v.arr.frozen) { + scr_regex_materialize_metadata(a); + return false; + } + scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_arr_named_put(target, "input", 5, scr_regex_source_input(src)); + scr_dyn_arr_named_put(target, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + return true; +} + /* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one * code unit otherwise. ASCII cannot contain a surrogate pair. */ static int scr_advance(const ScrRegexInput *input, int i, bool unicode) { @@ -713,12 +831,6 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool int count = lre_get_capture_count(bc); bool spans = lazy != 0 && !input.wide && (count >= 64 || ((mask | lazy) >> count) == 0); out = scr_arr_new_inline(SCR_ELEM_STR, (size_t)count + spans); - if (metadata) { - ScrDyn *props = scr_dyn_new_obj(); - scr_arr_ext(out)->metadata = props; - scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num((double)scr_capture_index(&input, capture[0]))); - scr_dyn_obj_set(props, "input", 5, scr_dyn_new_str(s)); - } /* A fresh dense array: every slot is written directly, then the * hole-free prefix is the run of materialized captures from 0. Its * storage pointers are fixed (the inline block never moves here), so @@ -750,6 +862,11 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool while (filled < n && present[filled] == SCR_ARR_VALUE) filled++; out->filled = (uint32_t)filled; out->len = n; + if (metadata) { + ScrRegexMatchSource *src = scr_regex_source_new(s, re); + scr_regex_defer_metadata(out, src, scr_capture_index(&input, capture[0])); + scr_regex_source_release(src); + } } scr_regex_input_dispose(&input); return out; /* +1, or NULL (no match) */ @@ -883,8 +1000,11 @@ double scr_regex_search(ScrStr *s, ScrRegex *re) { * Node's exact TypeError (catchable — replaceAll's stance). Empty matches * advance one unit, unicode-aware (AdvanceStringIndex). `indices`, when * non-NULL, receives each match's UTF-16 start index (the row's .index — - * the companion array the for-of-over-matchAll desugar reads). */ -static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices) { + * the companion array the for-of-over-matchAll desugar reads). With + * `metadata`, each row records the match source for its index, input and + * groups properties. */ +static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices, + bool metadata) { uint8_t *bc = scr_regex_bc(re); int re_flags = lre_get_flags(bc); if (!(re_flags & LRE_FLAG_GLOBAL)) { @@ -895,6 +1015,7 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices } bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); + ScrRegexMatchSource *src = metadata ? scr_regex_source_new(s, re) : NULL; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; @@ -906,30 +1027,51 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices if (scr_exec(&input, bc, pos) != 1) break; int start = scr_capture_index(&input, capture[0]); int end = scr_capture_index(&input, capture[1]); - ScrArr *row = scr_arr_new(SCR_ELEM_STR, (size_t)capture_count); + /* One allocation per row, like an exec result: every slot is written + * directly, then the hole-free prefix is the run of captures from 0. */ + ScrArr *row = scr_arr_new_inline(SCR_ELEM_STR, (size_t)capture_count); + uint64_t *data = row->data; + uint8_t *present = row->present; for (int k = 0; k < capture_count; k++) { const uint8_t *cs = capture[2 * k], *ce = capture[2 * k + 1]; if (cs == NULL || ce == NULL) { - scr_arr_set_undefined(row, (double)row->len); + data[k] = 0; + present[k] = SCR_ARR_UNDEFINED; } else { - scr_arr_push_ref( - row, scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce))); + ScrStr *text = + scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce)); + data[k] = (uint64_t)(uintptr_t)text; + present[k] = SCR_ARR_VALUE; } } + size_t filled = 0; + while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++; + row->filled = (uint32_t)filled; + row->len = (size_t)capture_count; + if (src) scr_regex_defer_metadata(row, src, start); scr_arr_push_ref(out, row); if (indices != NULL) scr_arr_push_f64(indices, (double)start); pos = end == start ? scr_advance(&input, end, unicode) : end; } + scr_regex_source_release(src); scr_regex_input_dispose(&input); return out; /* +1 (possibly empty — Node's no-match drain is []) */ } ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re) { - return scr_regex_match_all_core(s, re, NULL); + return scr_regex_match_all_core(s, re, NULL, true); +} + +ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re) { + return scr_regex_match_all_core(s, re, NULL, false); } ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices) { - return scr_regex_match_all_core(s, re, indices); + return scr_regex_match_all_core(s, re, indices, true); +} + +ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices) { + return scr_regex_match_all_core(s, re, indices, false); } /* ── replace / replaceAll ─────────────────────────────────────────────── */ diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h index e34e877de..af8ef4150 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -88,11 +88,23 @@ ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep); ScrArr *scr_regex_split(ScrStr *s, ScrRegex *re); ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit); /* matchAll drained eagerly: +1 string[][] of honest match slices; throws - * Node's TypeError on a non-global regex (catchable). */ + * Node's TypeError on a non-global regex (catchable). Each row carries its + * index, input and groups properties. */ ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re); +/* scr_regex_match_all for rows read only through their elements: the rows + * carry no index, input or groups properties. */ +ScrArr *scr_regex_match_all_captures(ScrStr *s, ScrRegex *re); /* matchAll + the companion-index drain: also pushes each match's UTF-16 * start index (f64) onto `indices` — the .index the for-of-over-matchAll * desugar reads. Same throw/result contract as scr_regex_match_all. */ ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices); +/* scr_regex_match_all_into for rows read only through their elements and + * the companion index: the rows carry no index, input or groups + * properties. */ +ScrArr *scr_regex_match_all_captures_into(ScrStr *s, ScrRegex *re, ScrArr *indices); +/* `.index` / `.input` of a match result: its pending match source or its + * metadata answers, without building the metadata object; undefined when + * neither has the key. Borrows both; +1. */ +struct ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key); #endif /* SCR_REGEX_H */ diff --git a/tests/corpus/6010-match-metadata.ts b/tests/corpus/6010-match-metadata.ts new file mode 100644 index 000000000..8de5f6c4a --- /dev/null +++ b/tests/corpus/6010-match-metadata.ts @@ -0,0 +1,40 @@ +// index and input on matchAll rows and exec results: for-of bindings that +// read only captures and the index, bindings that escape, spreads, +// callbacks, stored drains and closures. +const text = "k1=v10; k22=v3; k3=v400;"; +const re = /(?k\d+)=(?v\d+);/g; + +for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val); +const kept: RegExpExecArray[] = []; +for (const m of text.matchAll(re)) { + kept.push(m); + console.log(m.index, m.input === text); +} +console.log(kept.map((m) => `${m.index}:${m[0]}`).join(" ")); + +const rows = [...text.matchAll(re)]; +console.log(rows.map((m) => m.index).join(","), rows[1]!.index, rows[2]!.input.length); +console.log(rows.filter((m) => m.index > 0).map((m) => m.groups!.key).join(",")); +console.log(rows.find((m) => m.index === 8)?.[2], rows.at(-1)!.index); +console.log(Array.from(text.matchAll(re), (m) => m.index + m[0].length).join(",")); + +const drained = text.matchAll(/v(\d+)/g); +for (const m of drained) console.log(m.index, m[1]); +const stored = text.matchAll(/k(\d)/g); +const later = (): string => [...stored].map((m) => m.index).join("|"); +console.log(later()); + +function startOf(m: RegExpExecArray): number { + return m.index; +} +console.log([...text.matchAll(/=/g)].map(startOf).join(",")); + +const e = /(?\d+);/.exec(text)!; +console.log(e.index, e.input === text, e[1], e.groups!.n); +const g = text.match(/k\d+/g)!; +console.log(g.join(","), g.length); +const plain = [..."aXbX".matchAll(/X/g)]; +console.log(plain.map((m) => m.index).join(","), plain.length); +console.log([...text.matchAll(re)].map((m) => m[1]).join(","), [...text.matchAll(re)].length); +console.log(Array.from(text.matchAll(re), (m) => m.groups!.key + m.length).join(",")); +for (const m of [...text.matchAll(re)].slice(1)) console.log(m[2], m.groups!.val); diff --git a/tests/corpus/6011-match-metadata-js.js b/tests/corpus/6011-match-metadata-js.js new file mode 100644 index 000000000..b67e11069 --- /dev/null +++ b/tests/corpus/6011-match-metadata-js.js @@ -0,0 +1,31 @@ +// Match metadata on matchAll rows and exec results in JavaScript: index, +// input and groups through callbacks, untyped functions, destructuring, +// Object.keys and spread copies. +const text = "k1=v10; k22=v3; k3=v400;"; +const re = /(?k\d+)=(?v\d+)(?!)?;/g; +const rows = [...text.matchAll(re)]; +console.log(rows.map((m) => m.index).join(","), rows.map((m) => m.input.length).join(",")); +console.log(rows.map(({ index, groups }) => `${index}=${groups.key}`).join(" ")); + +function describe(m) { + return [m.index, m.input === text, m.groups.key, m.groups.bang, Object.keys(m).join("|")]; +} +for (const m of text.matchAll(re)) console.log(describe(m)); +console.log(rows.map((m) => describe(m)[0]).join(",")); + +const first = rows[0]; +const copy = { ...first }; +console.log(Object.keys(copy).join(","), copy.index, copy.groups.val, Object.getPrototypeOf(first.groups) === null); +console.log(JSON.stringify(first), first.length); + +const dup = [..."a1-b".matchAll(/(?[a-z])|(?\d)|-/g)]; +console.log(dup.map((m) => describe2(m)).join(" ")); +function describe2(m) { + return `${m.index}:${m.groups.v}`; +} + +const pick = (m) => m && [m.index, m.groups]; +console.log(pick(/(?\d)(?x)?/.exec("ab3")), pick(/b/.exec("ab")), pick(/z/.exec("ab"))); +console.log(pick("a1".match(/(?\d)/)), Object.keys("a1a".match(/a/g)).join(",")); +const none = [..."ab".matchAll(/[a-z]/g)]; +console.log(none.map((m) => m.groups === undefined && m.index >= 0).join(",")); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index e211a263b..cda819b0e 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2199,6 +2199,8 @@ "5996-rejections-between-macrotasks.cjs": 3034, "600-closures-basic.ts": 2349, "601-closures-loops.ts": 1788, + "6010-match-metadata.ts": 3990, + "6011-match-metadata-js.js": 3660, "602-closures-identity-recursion.ts": 2372, "6020-class-extends-function-constructor.js": 4300, "6021-asserted-literal-union-arm.ts": 3400, @@ -5236,6 +5238,8 @@ "5996-rejections-between-macrotasks.cjs": 22172, "600-closures-basic.ts": 2534, "601-closures-loops.ts": 2297, + "6010-match-metadata.ts": 5450, + "6011-match-metadata-js.js": 4890, "602-closures-identity-recursion.ts": 2427, "6020-class-extends-function-constructor.js": 7500, "6021-asserted-literal-union-arm.ts": 7200, From 8aa69ad16912c7494fcf98a4589d935c879ea1e5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:12:28 -0700 Subject: [PATCH 05/67] Stop checking the storage fill of early-reachable let globals A top-level `let x;` in a JavaScript module holds a checked dynamic value that the module %init fills with undefined at entry. When the module's early-running code (a published function, a function declaration called above the binding) can reach the binding, it also gets an initialization flag, and every write before the flag is set is TDZ-checked. The entry fill was rewritten like a source write, so the module threw "Cannot access 'x' before initialization" before its first statement ran. That broke json5, cliui and cosmiconfig on load. Mark the fill as initializing when the global has a flag: it is storage setup, not the source write, so it neither runs the check nor sets the flag. Accesses that really happen before the declaration still throw. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-modules.ts | 11 ++++++++- .../6180-cjs-let-early-reachable/main.js.json | 7 ++++++ .../6181-esm-let-early-reachable/main.js.json | 7 ++++++ .../6180-cjs-let-early-reachable/lib.js | 24 +++++++++++++++++++ .../6180-cjs-let-early-reachable/main.js | 11 +++++++++ .../6180-cjs-let-early-reachable/package.json | 1 + .../6181-esm-let-early-reachable/lib.js | 19 +++++++++++++++ .../6181-esm-let-early-reachable/main.js | 8 +++++++ .../6181-esm-let-early-reachable/package.json | 1 + 9 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json create mode 100644 tests/corpus/6180-cjs-let-early-reachable/lib.js create mode 100644 tests/corpus/6180-cjs-let-early-reachable/main.js create mode 100644 tests/corpus/6180-cjs-let-early-reachable/package.json create mode 100644 tests/corpus/6181-esm-let-early-reachable/lib.js create mode 100644 tests/corpus/6181-esm-let-early-reachable/main.js create mode 100644 tests/corpus/6181-esm-let-early-reachable/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3baab9dd0..df6677d5b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -2791,7 +2791,16 @@ export function lowerFileInit( const wrapped = g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0); if (wrapped) { - header.push({ kind: "assign", localId: g.id, value: wrapped, loc: loc0 }); + // A let global whose early reads are checked (initFlag) only + // gets its storage filled here: this is not the source write, so + // it must neither run the TDZ check nor set the flag. + header.push({ + kind: "assign", + localId: g.id, + value: wrapped, + ...(g.initFlag !== undefined ? { initializes: true as const } : {}), + loc: loc0, + }); } } const prelude = lowerer.jsonInitActions.get(sf) ?? []; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json new file mode 100644 index 000000000..c83e326cd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6180-cjs-let-early-reachable/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6180-cjs-let-early-reachable/lib.js", + "/tests/corpus/6180-cjs-let-early-reachable/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json new file mode 100644 index 000000000..cc09432a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6181-esm-let-early-reachable/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6181-esm-let-early-reachable/lib.js", + "/tests/corpus/6181-esm-let-early-reachable/main.js" + ], + "diags": [] +} diff --git a/tests/corpus/6180-cjs-let-early-reachable/lib.js b/tests/corpus/6180-cjs-let-early-reachable/lib.js new file mode 100644 index 000000000..ce4300b1f --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/lib.js @@ -0,0 +1,24 @@ +'use strict'; + +let source; +const parse = function parse(text) { + source = text; + return lex(); +}; +module.exports = parse; + +try { + console.log('early', parse('early')); +} catch (e) { + console.log('early', e instanceof ReferenceError, e.message); +} + +let state; +let count; +function lex() { + state = state === undefined ? 'fresh' : 'seen'; + count = count === undefined ? 1 : count + 1; + return source + ':' + state + ':' + count; +} + +console.log('loaded', state, count); diff --git a/tests/corpus/6180-cjs-let-early-reachable/main.js b/tests/corpus/6180-cjs-let-early-reachable/main.js new file mode 100644 index 000000000..58c0e5044 --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/main.js @@ -0,0 +1,11 @@ +// A CommonJS module's top-level `let x;` bindings that a published +// function can reach before their declarations run. Node only throws +// for an access that really happens before the declaration; once the +// module body has run, every call sees the bindings (undefined until +// assigned). +'use strict'; + +const parse = require('./lib.js'); + +console.log(parse('a'), parse('b')); +console.log(parse('c')); diff --git a/tests/corpus/6180-cjs-let-early-reachable/package.json b/tests/corpus/6180-cjs-let-early-reachable/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6180-cjs-let-early-reachable/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6181-esm-let-early-reachable/lib.js b/tests/corpus/6181-esm-let-early-reachable/lib.js new file mode 100644 index 000000000..47a688101 --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/lib.js @@ -0,0 +1,19 @@ +let mixin; +export function cliui(opts) { + if (opts !== undefined) mixin = opts; + return describe(); +} + +let earlyResult; +try { + earlyResult = cliui({ width: 1 }); +} catch (e) { + earlyResult = `${e.name}: ${e.message}`; +} +export const early = earlyResult; + +let calls; +function describe() { + calls = calls === undefined ? 1 : calls + 1; + return `${calls} ${mixin === undefined ? 'none' : mixin.width}`; +} diff --git a/tests/corpus/6181-esm-let-early-reachable/main.js b/tests/corpus/6181-esm-let-early-reachable/main.js new file mode 100644 index 000000000..a27a418e2 --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/main.js @@ -0,0 +1,8 @@ +// An ES module's top-level `let x;` bindings that an exported function +// can reach before their declarations run. Only an access that really +// happens before the declaration throws; after the module body ran, +// calls see the bindings (undefined until assigned). +import { cliui, early } from './lib.js'; + +console.log(early); +console.log(cliui({ width: 3 }), cliui(undefined)); diff --git a/tests/corpus/6181-esm-let-early-reachable/package.json b/tests/corpus/6181-esm-let-early-reachable/package.json new file mode 100644 index 000000000..5ffd9800b --- /dev/null +++ b/tests/corpus/6181-esm-let-early-reachable/package.json @@ -0,0 +1 @@ +{ "type": "module" } From 6d6a140064aef2537e6a35ca598dca279dc73e36 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:16:48 -0700 Subject: [PATCH 06/67] Lower a JavaScript class method that reads itself as a value once A reflected JavaScript class method lowers its lambda the first time its value is read, and caches the closure only after the body lowered. A method whose own body reads the method as a value, like i18next's `instance.createInstance = I18n.createInstance`, reentered the lowering for the same method until the compiler overflowed its stack. Record the method while its lambda lowers; lowerLambda names the closure it will produce, and a read of the method inside its own body takes that closure, so every read sees the same function. When the body later changes the closure (its type is settled by an inferred return, or it captures), the self read is refused with a named diagnostic instead. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/class-method-values.ts | 37 +++++++++++++++++++ .../src/frontend/lowering/lower-functions.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 9 +++++ .../6182-class-method-self-value.js.json | 6 +++ tests/corpus/6182-class-method-self-value.js | 28 ++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json create mode 100644 tests/corpus/6182-class-method-self-value.js diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 7c8770021..50278c3b7 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -379,12 +379,48 @@ function reflectedMethodValue( needsNativeReceiver = true; }); if (needsNativeReceiver) return null; + // The method reads itself as a value inside its own body: it is the + // closure its lowering in progress produces. + if (lowerer.methodLambdasInProgress.has(member)) { + const pending = lowerer.methodLambdasInProgress.get(member); + if (!pending) { + lowerer.unsupported( + "SC1090", + member, + "class methods that read their own value while their signature is computed", + ); + } + pending.selfReads++; + return { + kind: "closure", + fnName: pending.fnName, + captures: [], + type: pending.type, + loc: locOf(member), + }; + } const context = newFnCtx(false, null, null, DYN); const previousClass = lowerer.currentClass; lowerer.currentClass = info; lowerer.fnStack.push(context); + lowerer.methodLambdasInProgress.set(member, null); try { const value = lowerer.lowerLambda(member); + const pending = lowerer.methodLambdasInProgress.get(member); + if ( + pending && + pending.selfReads > 0 && + (value.kind !== "closure" || + value.fnName !== pending.fnName || + value.captures.length > 0 || + !typeEquals(value.type, pending.type)) + ) { + lowerer.unsupported( + "SC1090", + member, + "class methods that read their own value inside their body when the method's value type is only settled by its body", + ); + } 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; @@ -395,6 +431,7 @@ function reflectedMethodValue( cache.set(method, value); return value; } finally { + lowerer.methodLambdasInProgress.delete(member); lowerer.fnStack.pop(); lowerer.currentClass = previousClass; } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 2991c0d39..d1707101e 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,6 +270,8 @@ export function lowerLambda( : lowerer.bodyReturnType(isAsync, funcType.ret); const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn); + if (lowerer.methodLambdasInProgress.has(node)) + lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 }); const inferredSignature = isJsSourceFile(node.getSourceFile()) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index bf4ed5464..adca3dfc5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2332,6 +2332,15 @@ export class Lowerer { /** Method adapter names are owned by class-method-values. Looking them * up must not scan and retain every unrelated lifted function. */ readonly classMethodValueHelpers = new Set(); + /** Reflected class methods whose lambda is lowering right now (null + * until lowerLambda named it), with the closure it will produce: a + * method that reads itself as a value inside its own body + * (`o.create = K.create`) takes this closure instead of lowering the + * method again. */ + readonly methodLambdasInProgress = new Map< + ts.Node, + { fnName: string; type: IrType; selfReads: number } | null + >(); readonly classMethodValueSelections = new Map< string, import("./class-method-values.js").ClassMethodValueSelection diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json new file mode 100644 index 000000000..7c8016a0e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6182-class-method-self-value.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6182-class-method-self-value.js" + ], + "diags": [] +} diff --git a/tests/corpus/6182-class-method-self-value.js b/tests/corpus/6182-class-method-self-value.js new file mode 100644 index 000000000..a5527dbe7 --- /dev/null +++ b/tests/corpus/6182-class-method-self-value.js @@ -0,0 +1,28 @@ +// JavaScript class methods that read themselves as a value inside their +// own body: i18next's `instance.createInstance = I18n.createInstance`. +// The value is the same function every other read of the method sees. + +class Registry { + /** @param {string} name */ + constructor(name) { + this.name = name; + /** @type {((name: string) => Registry) | undefined} */ + this.create = undefined; + } + + /** @param {string} name */ + static create(name) { + const registry = new Registry(name); + registry.create = Registry.create; + return registry; + } + + describe() { + return `${this.name} ${typeof this.create}`; + } +} + +const a = Registry.create('a'); +const b = a.create?.('b') ?? a; +console.log(a.describe(), b.describe()); +console.log(a.create === Registry.create, b.create === a.create); From 7ab6e8c6262e1d392aca4b518932743409794856 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:11 -0700 Subject: [PATCH 07/67] Store typed-array elements through storage wider than a typed array `b[i] = v` lowered to bytesSet whenever the checker's flow type of `b` was a typed array, even when the binding's storage was wider. A hoisted JavaScript `var` holds `bytes | undefined`, so the plain `var dst = Buffer.alloc(n); dst[i] = x` (ecdsa-sig-formatter, dotenv) and a field assigned after construction (@noble/hashes' HashMD buffer) failed IR validation with SC9001 "bytesSet on non-bytes union". For `bytes | undefined` storage with a number index, evaluate the receiver, index and value in order, then throw Node's TypeError for an undefined receiver or take the typed store. Any other storage takes the checked-dynamic store, which applies the value the receiver holds; a type with no checked-dynamic form is refused with a named diagnostic. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 85 +++++++++++++++++++ .../6184-js-bytes-write-wider-storage.js.json | 6 ++ .../6184-js-bytes-write-wider-storage.js | 57 +++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json create mode 100644 tests/corpus/6184-js-bytes-write-wider-storage.js diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591d..d8d969973 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -7328,6 +7328,89 @@ function lowerGuardedRecordWrite( }; } +/** `b[i] = v` where the checker's flow type of `b` is a typed array but + * its storage is wider. A hoisted JavaScript `var` (or a field written + * after construction) holds `bytes | undefined`: the receiver, index and + * value evaluate in order, then an undefined receiver throws Node's + * TypeError and a present one takes the typed store. Any other storage (a + * `var` later reassigned a string, a checked-dynamic value) takes the + * checked-dynamic store, which applies whatever value the receiver holds. */ +function lowerWiderBytesWrite( + lowerer: Lowerer, + expr: ts.BinaryExpression, + target: ts.ElementAccessExpression, + recv: IrExpr, +): IrStmt { + const loc = locOf(expr); + if (recv.type.kind === "union") { + const unionId = recv.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + const undefTag = lowerer.armTag(unionId, UNDEFINED_T); + const bytes = arms?.length === 2 && undefTag >= 0 ? arms[1 - undefTag]! : null; + const keyType = lowerer.mapTypeOf(lowerer.typeOf(target.argumentExpression)); + const index = + bytes?.kind === "bytes" && keyType?.kind === "f64" + ? lowerer.lowerExpr(target.argumentExpression) + : null; + // An index stored wider than a number (a hoisted `var` counter) keeps + // the checked-dynamic store below, which converts the key exactly. + if (bytes?.kind === "bytes" && index?.type.kind === "f64") { + const stmts: IrStmt[] = []; + const stable = stabilized(lowerer, recv, "%bytesReceiver", stmts); + const key = stabilized(lowerer, index, "%bytesIndex", stmts); + const value = stabilized( + lowerer, + lowerer.lowerExprExpecting(expr.right, F64), + "%bytesValue", + stmts, + ); + const name = lowerRecordPropertyKey(lowerer, key, target.argumentExpression); + const present: IrExpr = { + kind: "unionNarrow", + unionId, + tag: 1 - undefTag, + value: stable, + type: bytes, + loc, + }; + return { + kind: "block", + body: [ + ...stmts, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId, + tag: undefTag, + negated: false, + value: stable, + type: BOOL, + loc, + }, + then: [{ kind: "exprStmt", expr: undefinedReceiverThrow(name, "set", VOID, loc), loc }], + else_: [{ kind: "bytesSet", arr: present, index: key, value, loc }], + loc, + }, + ], + loc, + }; + } + } + if (!lowerer.dynConvertible(recv.type)) { + lowerer.unsupported( + "SC1090", + target.expression, + `typed-array element writes through '${lowerer.fmt(recv.type)}' storage`, + ); + } + return { + kind: "exprStmt", + expr: lowerDynMemberAssignment(lowerer, expr, lowerer.coerceInto(target.expression, recv, DYN)), + loc, + }; +} + /** `r[k]` over a (non-tuple) record shape. A LITERAL key naming a declared * field is the bracket spelling of field access (the ONLY spelling tsc * permits for signature-declared fields under @@ -8374,6 +8457,8 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): // the runtime coerces JS-exactly; invalid indices are ignored. if (receiverIr?.kind === "bytes") { const recv = lowerer.lowerExpr(target.expression); + // The checker's flow type is a typed array, but the storage is wider. + if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv); const index = lowerer.lowerExpr(target.argumentExpression); if (index.type.kind === "dyn" && recv.type.kind === "bytes") { return { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json new file mode 100644 index 000000000..7c4e146e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6184-js-bytes-write-wider-storage.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6184-js-bytes-write-wider-storage.js" + ], + "diags": [] +} diff --git a/tests/corpus/6184-js-bytes-write-wider-storage.js b/tests/corpus/6184-js-bytes-write-wider-storage.js new file mode 100644 index 000000000..ff0e3295f --- /dev/null +++ b/tests/corpus/6184-js-bytes-write-wider-storage.js @@ -0,0 +1,57 @@ +// Typed-array element writes through storage wider than the checker's +// flow type: a hoisted `var` (ecdsa-sig-formatter's joseToDer) and a +// field that holds undefined until it is assigned (@noble/hashes' HashMD +// buffer). Each write stores into the typed array the receiver holds, or +// throws Node's TypeError for an undefined one after the index and the +// value evaluated. +'use strict'; + +/** @param {number} n */ +function der(n) { + var dst = Buffer.alloc(n); + var offset = 0; + dst[offset++] = 48; + dst[offset++] = n; + dst[offset] = 257; + return dst.toString('hex'); +} + +/** @param {number} n */ +function grow(n) { + var dst = Buffer.alloc(n); + for (var i = 0; i < n; i++) dst[i] = i * 3; + dst = Buffer.concat([dst, Buffer.from([255])]); + dst[0] = 7; + return dst.toString('hex'); +} + +console.log(der(3), grow(4)); + +/** @param {number} v */ +function note(v) { + console.log('value', v); + return v; +} + +class Hash { + buffer; + /** @param {number} n */ + constructor(n) { + if (n > 0) this.buffer = new Uint8Array(n); + this.pos = 0; + } + digest() { + const { buffer } = this; + let { pos } = this; + buffer[pos++] = note(0x80); + buffer[pos] = 300; + return Array.from(buffer).join(','); + } +} + +console.log(new Hash(4).digest()); +try { + console.log(new Hash(0).digest()); +} catch (e) { + console.log(e instanceof TypeError, e.message); +} From 21ae43bd88ed3d616dc542ff2e55b1fc529f6726 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:17 -0700 Subject: [PATCH 08/67] Guard keyed reads of a null-initialized record table A keyed read whose checker type is a record but whose storage is `record | null` (iconv-lite's `module.exports.encodings = null`, filled on first use and read as `encodings[enc]`) built a recordKeyGet on the union and failed IR validation with SC9001. The optional-receiver guard that keyed reads already use for `record | undefined` now also takes a null arm for string and number keys: the receiver and key evaluate once, a null receiver throws Node's "Cannot read properties of null (reading 'k')", and a present one reads the record. Other keys keep their checked-dynamic read, and keyed writes are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 44 ++++++++++++++----- .../6185-cjs-null-table-keyed-read.cjs.json | 6 +++ .../corpus/6185-cjs-null-table-keyed-read.cjs | 23 ++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json create mode 100644 tests/corpus/6185-cjs-null-table-keyed-read.cjs diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d8d969973..c54341df8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -7173,25 +7173,38 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre /** A record receiver that an outer keyed read left optional * (`nested[a][b]`, where `nested[a]` can miss). The receiver evaluates * once into `stmts`; `present` is its record arm, valid only once the - * access has tested `stable` against the undefined arm. */ + * access has tested `stable` against the undefined (or, for reads, null) + * arm. */ interface OptionalRecordReceiver { stmts: IrStmt[]; stable: IrExpr; present: IrExpr; unionId: string; undefTag: number; + nullish: "undefined" | "null"; } function optionalRecordReceiver( lowerer: Lowerer, value: IrExpr, loc: SrcLoc, + allowNull = false, ): OptionalRecordReceiver | null { if (value.type.kind !== "union") return null; const unionId = value.type.unionId; - const undefTag = lowerer.armTag(unionId, UNDEFINED_T); - if (undefTag < 0 || lowerer.unions.get(unionId)?.arms.length !== 2) return null; - const record = lowerer.stripUndefinedArm(value.type); + const arms = lowerer.unions.get(unionId)?.arms; + if (arms?.length !== 2) return null; + let undefTag = lowerer.armTag(unionId, UNDEFINED_T); + let nullish: OptionalRecordReceiver["nullish"] = "undefined"; + // A null-initialized slot assigned a record later (iconv-lite's + // `encodings = null` table, filled on first use): the checker narrows + // the read to the record, the storage keeps the null arm. + if (undefTag < 0 && allowNull) { + undefTag = lowerer.armTag(unionId, NULL_T); + nullish = "null"; + } + if (undefTag < 0) return null; + const record = arms[1 - undefTag]!; if (record.kind !== "record") return null; const stmts: IrStmt[] = []; const stable = stabilized(lowerer, value, "%recordReceiver", stmts); @@ -7203,7 +7216,7 @@ function optionalRecordReceiver( type: record, loc, }; - return { stmts, stable, present, unionId, undefTag }; + return { stmts, stable, present, unionId, undefTag, nullish }; } /** `value`, evaluated once at this point of `out`. */ @@ -7226,14 +7239,16 @@ function undefinedReceiver(receiver: OptionalRecordReceiver, loc: SrcLoc): IrExp }; } -/** Node's TypeError for a keyed read or write on an undefined receiver. */ +/** Node's TypeError for a keyed read or write on an undefined (or null) + * receiver. */ function undefinedReceiverThrow( key: IrExpr, action: "read" | "set", type: IrType, loc: SrcLoc, + nullish: OptionalRecordReceiver["nullish"] = "undefined", ): IrExpr { - const head = `Cannot ${action} properties of undefined (${action === "read" ? "reading" : "setting"} '`; + const head = `Cannot ${action} properties of ${nullish} (${action === "read" ? "reading" : "setting"} '`; const message: IrExpr = { kind: "strConcat", left: { @@ -7263,8 +7278,9 @@ function guardedRecordAccess( lowerer: Lowerer, access: ts.ElementAccessExpression, rawObj: IrExpr, + allowNull = false, ): { receiver: OptionalRecordReceiver; key: IrExpr; name: IrExpr } | null { - const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression)); + const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression), allowNull); if (!receiver) return null; const keyNode = access.argumentExpression; const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(lowerer, keyNode); @@ -7450,7 +7466,15 @@ export function lowerRecordKeyRead( // evaluates the receiver before the key). const obj = lowerer.lowerExpr(expr.expression); { - const guarded = guardedRecordAccess(lowerer, expr, obj); + // A null arm is guarded only for string and number keys: other keys + // already take the checked-dynamic read, which handles null itself. + // Only a receiver with a null arm asks about the key. + let allowNull = false; + if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, NULL_T) >= 0) { + const keyType = litKey !== null ? STRING : lowerer.mapTypeOf(lowerer.typeOf(keyNode)); + allowNull = keyType?.kind === "string" || keyType?.kind === "f64"; + } + const guarded = guardedRecordAccess(lowerer, expr, obj, allowNull); if (guarded) { const { receiver, key, name } = guarded; const read = lowerer.withExpressionOverride(expr.expression, receiver.present, () => @@ -7466,7 +7490,7 @@ export function lowerRecordKeyRead( result: { kind: "ternary", cond: undefinedReceiver(receiver, loc), - then: undefinedReceiverThrow(name, "read", read.type, loc), + then: undefinedReceiverThrow(name, "read", read.type, loc, receiver.nullish), else_: read, type: read.type, loc, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json new file mode 100644 index 000000000..c3a6e5492 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6185-cjs-null-table-keyed-read.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6185-cjs-null-table-keyed-read.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6185-cjs-null-table-keyed-read.cjs b/tests/corpus/6185-cjs-null-table-keyed-read.cjs new file mode 100644 index 000000000..41bfb1f1f --- /dev/null +++ b/tests/corpus/6185-cjs-null-table-keyed-read.cjs @@ -0,0 +1,23 @@ +// A keyed read of an exported table that starts out null and is filled +// later (iconv-lite's `encodings`). The checker narrows the read to the +// table, while the export's storage keeps the null arm. +'use strict'; + +/** @type {{ [name: string]: string } | null} */ +module.exports.encodings = null; + +/** @param {string} name */ +function note(name) { + console.log('key', name); + return name; +} + +/** @param {string} name */ +const lookup = function lookup(name) { + if (!module.exports.encodings) return 'none'; + return module.exports.encodings[note(name)]; +}; + +console.log(lookup('utf8')); +module.exports.encodings = { utf8: 'UTF-8', latin1: 'ISO-8859-1' }; +console.log(lookup('utf8'), lookup('latin1'), lookup('ascii')); From 74646b783ff75d1dabdf0d10be1f01c474913614 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:18 -0700 Subject: [PATCH 09/67] Check a dynamic `{ encoding }` option into its record for encoded reads `fs.readFileSync(file, { encoding })` and `fs.promises.readFile` read the encoding field straight off the lowered options literal. When the encoding value is checked-dynamic (dotenv's `t = e.encoding`), the literal lowers to a dynamic object, and the record field read failed IR validation with SC9001 "recordGet receiver: expected record, got dyn". Check such a literal into the `{ encoding: string }` record the checker describes before reading the field. Co-Authored-By: Claude Opus 5.5 --- .../lowering/builtins/filesystem-calls.ts | 13 +++++++++-- ...186-js-fs-encoding-dynamic-option.cjs.json | 6 +++++ .../6186-js-fs-encoding-dynamic-option.cjs | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json create mode 100644 tests/corpus/6186-js-fs-encoding-dynamic-option.cjs diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index 754eb6aa9..d6864a2d9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -23,6 +23,7 @@ import { VOID, arrayOf, funcOf, + typeEquals, } from "../../../ir/ir.js"; import { lowerBuiltinOptionalDefault, optionMember, literalBoolOptions } from "./arguments.js"; import { @@ -603,6 +604,14 @@ export function lowerFsReadDescriptorCall( return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc }; } +/** The `{ encoding }` options record. A literal whose value is checked- + * dynamic (an encoding variable reassigned from an untyped source) lowers + * to a dynamic object: check it into the record the checker describes. */ +function encodingOptions(lowerer: Lowerer, options: ts.Expression, type: IrType): IrExpr { + const value = lowerer.lowerExpr(options); + return typeEquals(value.type, type) ? value : lowerer.coerceInto(options, value, type); +} + /** Encoding-bearing reads share Buffer's decoder and runtime alias checks. * Keep the read and encoding evaluation in source order in one helper. */ export function lowerFsEncodedRead( @@ -631,7 +640,7 @@ export function lowerFsEncodedRead( ? lowerer.lowerExprExpecting(options, STRING) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, @@ -692,7 +701,7 @@ export function lowerFsPromisesEncodedRead( ? lowerer.lowerExprExpecting(options, STRING) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json new file mode 100644 index 000000000..7e6f4d8d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs" + ], + "diags": [] +} diff --git a/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs b/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs new file mode 100644 index 000000000..cea0fae8f --- /dev/null +++ b/tests/corpus/6186-js-fs-encoding-dynamic-option.cjs @@ -0,0 +1,22 @@ +// readFileSync with a `{ encoding }` literal whose value comes from an +// untyped source (dotenv's configDotenv): the option object is checked +// into the record the checker describes. +'use strict'; + +const fs = require('fs'); + +/** + * @param {string} file + * @param {any} options + */ +function read(file, options) { + let encoding = 'utf8'; + if (options && options.encoding) encoding = options.encoding; + return fs.readFileSync(file, { encoding }); +} + +const latin1 = read(__filename, { encoding: 'latin1' }); +const utf8 = read(__filename, null); +if (typeof latin1 === 'string' && typeof utf8 === 'string') { + console.log(latin1.length === utf8.length, utf8.split('\n')[0]); +} From 3b9272e8f899192f4c7aa448e9929ab64b53bc73 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:33:18 -0700 Subject: [PATCH 10/67] Keep Date slots out of runtime-optional undefined widening The runtime-optional analysis widens a parameter or binding that can observe undefined with an undefined arm. For a Date it interned the union `Date | undefined`, which is not a valid union (Date is not a union arm kind), and the whole program failed IR validation with SC9001 "union uN: arm 0 is date" (morgan's date token and clfdate). Like generators, a Date keeps its type: the canonical undefined-arm helper already refuses Date, and a site that would store undefined into the slot keeps its own diagnostic. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 6 +++- .../corpus/6183-js-date-optional-slot.js.json | 6 ++++ tests/corpus/6183-js-date-optional-slot.js | 36 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json create mode 100644 tests/corpus/6183-js-date-optional-slot.js diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index adca3dfc5..5d7713728 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -4353,7 +4353,11 @@ export class Lowerer { const addUndefined = (t: IrType): IrType => { if (t.kind === "union") return this.armTag(t.unionId, UNDEFINED_T) >= 0 ? t : (this.withUndefinedArmOf(t) ?? t); - if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator") return t; + // A Date has no undefined-armed union (it is not a union arm kind): + // like a generator it keeps its type, and the site that would store + // undefined into it keeps its own diagnostic. + if (t.kind === "void" || t.kind === "dyn" || t.kind === "generator" || t.kind === "date") + return t; return this.withUndefinedArm(t); }; const fieldName = (node: ts.PropertyAccessExpression): string => node.name.text; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json new file mode 100644 index 000000000..ded598761 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6183-js-date-optional-slot.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6183-js-date-optional-slot.js" + ], + "diags": [] +} diff --git a/tests/corpus/6183-js-date-optional-slot.js b/tests/corpus/6183-js-date-optional-slot.js new file mode 100644 index 000000000..9c37d85b9 --- /dev/null +++ b/tests/corpus/6183-js-date-optional-slot.js @@ -0,0 +1,36 @@ +// A Date binding and parameter that the runtime-optional analysis would +// widen with an undefined arm (morgan's date token). Date values have no +// undefined-armed union, so the bindings keep their Date type. +'use strict'; + +/** @type {Record string | undefined>} */ +const tokens = {}; + +/** + * @param {string} name + * @param {(format: string | undefined) => string | undefined} fn + */ +function token(name, fn) { + tokens[name] = fn; +} + +token('date', function getDateToken(format) { + var date = new Date(86400000 * 400); + switch (format || 'iso') { + case 'clf': + return clfdate(date); + case 'iso': + return date.toISOString(); + } +}); + +/** + * @param {Date} dateTime + * @return {string} + */ +function clfdate(dateTime) { + return dateTime.getUTCDate() + '/' + dateTime.getUTCMonth() + '/' + dateTime.getUTCFullYear(); +} + +const date = tokens['date']; +if (date) console.log(date('clf'), date('iso'), date(undefined), date('other')); From 92310a68fc573e7c6583e13d6e1c872bc0a1962d Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:43:15 -0700 Subject: [PATCH 11/67] Keep statement fences in functions hoisted during a speculative lowering A reference to a later function declaration in the same function scope lowers that declaration on the spot (predeclareForwardFnDecl), and the result is kept. When the reference sat inside a speculative attempt (tryLowerExpression's temporary diagnostic sink, here the native function assignment in bignumber.js's `var P = BigNumber.prototype = {...}`), the hoisted body lowered under that sink: its failing JavaScript statements were dropped instead of becoming runtime fences. bignumber's `isBigNumber` lost its only `return`, so every program using the package aborted at load with SC9002 "a non-void function completed without returning", and its constructor silently lost its instanceof check. Lower the hoisted declaration without the probe's sink, as eager generic instances already do. A declaration that fails as a whole is not kept, so its diagnostics go back to the probe. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 11 ++++ .../speculative-hoisted-functions.test.ts | 64 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/compiler/test/speculative-hoisted-functions.test.ts diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 0f1b606ef..65f39c6ab 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -935,12 +935,23 @@ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): bo entry.ctx.hoistedFnDecls.add(decl); const fnTail = lowerer.fnStack.splice(depth + 1); const scopeTail = entry.ctx.scopes.splice(frameIdx + 1); + // The hoisted function is kept even when the reference lowers inside + // a speculative attempt (a probe's temporary diagnostic sink): lower + // it without that sink, so its failing JS statements keep their + // runtime fences instead of vanishing from a body that is kept. A + // declaration that fails as a whole is not kept: its diagnostics go + // back to the probe. + const previousSink = lowerer.diagSink; + const diagsBefore = lowerer.diags.length; + lowerer.diagSink = null; try { entry.out.push(lowerer.lowerNestedFunctionDecl(decl)); } catch (e) { entry.ctx.hoistedFnDecls.delete(decl); + if (previousSink !== null) previousSink.push(...lowerer.diags.splice(diagsBefore)); throw e; } finally { + lowerer.diagSink = previousSink; entry.ctx.scopes.push(...scopeTail); lowerer.fnStack.push(...fnTail); } diff --git a/packages/compiler/test/speculative-hoisted-functions.test.ts b/packages/compiler/test/speculative-hoisted-functions.test.ts new file mode 100644 index 000000000..a58310b2a --- /dev/null +++ b/packages/compiler/test/speculative-hoisted-functions.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrModule } from "../src/ir/ir.js"; + +// bignumber.js's clone(): `P = BigNumber.prototype = {...}` in a `var` +// list above the constructor. The initializer lowers speculatively first, +// which hoists the constructor and the helper it calls. Those bodies are +// kept, so their failing statements must keep runtime fences; dropping +// them left `isNum` without its return (an SC9002 trap at load). +const source = ` +function clone(config) { + var P = (Num.prototype = { constructor: Num, toString: null }), + unit = config ? 2 : 1; + function Num(v) { + var x = this; + if (!(x instanceof Num)) return new Num(v); + x.v = isNum(v) ? 0 : v * unit; + } + function isNum(v) { + return v instanceof Num || (!!v && v._isNum === true); + } + P._isNum = true; + return Num; +} +const Num = clone(); +console.log(typeof Num); +`; + +async function lower(): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-speculative-hoisted-")); + try { + const entry = join(dir, "main.mjs"); + const outPath = join(dir, "main.ir.json"); + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fenceCodes(node: unknown, out: string[] = []): string[] { + if (Array.isArray(node)) { + for (const item of node) fenceCodes(item, out); + } else if (node !== null && typeof node === "object") { + const record = node as Record; + if (record["kind"] === "runtimeFence") out.push(String(record["code"])); + for (const [key, value] of Object.entries(record)) if (key !== "loc") fenceCodes(value, out); + } + return out; +} + +test("speculatively hoisted JavaScript functions keep their statement fences", async () => { + const mod = await lower(); + const isNum = mod.functions.find((fn) => fn.name.endsWith("_isNum")); + expect(isNum).toBeDefined(); + expect(fenceCodes(isNum!.body)).toEqual(["SC1090"]); + expect(mod.functions.flatMap((fn) => fenceCodes(fn.body))).not.toContain("SC9002"); +}); From beddc5f94ab61c0cfc0ad2503d971e83199eb1fa Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 12:52:04 -0700 Subject: [PATCH 12/67] Keep typescript-go's tuple-literal panic with the statements that ask `s.match(re) || []` contextually types the empty array by the tuple-like RegExpMatchArray, so typescript-go builds an empty tuple target and clones it as an array literal. The clone keeps ObjectFlagsTuple on TypeReference data, and the API's newTypeResponse panics (interface conversion to *TupleType) whenever it serializes the clone, for example as a member of `RegExpMatchArray | []`. Whole-program analyses asked for such union members first, so every program containing the pattern (moment.js, or a two-line TypeScript file) failed at 1:1 with SC0004. A union whose members typescript-go panics to serialize is now unmappable at the top of mapType, the any-array probe answers false for it, and the panic is remembered per type instead of re-requested. Queries that still need the members keep the source-anchored SC0004 on their own statement (a runtime fence in JavaScript). Other checker panics are unchanged. The panic itself is an upstream bug in internal/api/proto.go. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/frontend/lowering/lowerer.ts | 8 +++++++- packages/compiler/src/frontend/ts7/checker.ts | 10 ++++++++++ .../src/frontend/ts7/semantic-model.ts | 20 +++++++++++++++++-- packages/compiler/src/frontend/type-mapper.ts | 11 +++++++++- .../6187-js-match-or-empty-array.js.json | 6 ++++++ tests/corpus/6187-js-match-or-empty-array.js | 20 +++++++++++++++++++ tests/diagnostics/checker-panic-tuple.ts | 5 +++-- .../__snapshots__/checker-panic-tuple.ts.txt | 18 ++++++++--------- 8 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json create mode 100644 tests/corpus/6187-js-match-or-empty-array.js diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 5d7713728..5bcf15b1a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -7746,7 +7746,13 @@ export class Lowerer { if (part.isUnionType()) return ts.constituentTypes(part).every(visit); return false; }; - return visit(t); + // A union typescript-go cannot describe is not proven an array. + try { + return visit(t); + } catch (e) { + if (ts.isUnreadableConstituentsPanic(e)) return false; + throw e; + } } /** The lowered static array/tuple value behind checkerAnyArray's diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 9d7634591..bf83b367c 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -120,6 +120,16 @@ export function constituentTypes(type: Type): readonly Type[] { return type.getTypes() ?? []; } +/** True for the panic typescript-go's API raises serializing a union's + * constituents (an empty tuple cloned as an array literal keeps the tuple + * flag without tuple data: `s.match(re) || []`). The union itself is + * readable; its members are not. */ +export function isUnreadableConstituentsPanic(e: unknown): boolean { + return ( + e instanceof Error && (e as { unreadableConstituents?: true }).unreadableConstituents === true + ); +} + /** Function-like declarations whose body is deferred until reachability * asks for it. Header prefetch walks their names, type parameters, params, * and return types but leaves the body for a later explicit body wave. */ diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 28625dd03..095021922 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -408,6 +408,7 @@ export class SemanticType { private trueType: number | false = false; private falseType: number | false = false; private constituents: SemanticType[] | undefined; + private constituentsPanic: Error | undefined; constructor( private readonly data: TypeResponse, @@ -497,12 +498,27 @@ export class SemanticType { this.project.ensureActive(); if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return undefined; - if (this.constituents === undefined) - this.constituents = this.project.fetchTypes(this.id, "getTypesOfType"); + if (this.constituentsPanic !== undefined) throw this.constituentsPanic; + if (this.constituents === undefined) { + try { + this.constituents = this.project.fetchTypes(this.id, "getTypesOfType"); + } catch (e) { + // typescript-go panics serializing some constituents (an empty + // tuple cloned as an array literal): answer every later read of + // this snapshot's type with the same panic instead of a new + // request. + if (e instanceof Error && e.message.startsWith("panic:")) { + (e as { unreadableConstituents?: true }).unreadableConstituents = true; + this.constituentsPanic = e; + } + throw e; + } + } return this.constituents; } dispose(): void { this.constituents = undefined; + this.constituentsPanic = undefined; } getBaseTypes(): SemanticType[] | undefined { this.project.ensureActive(); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..e0558ae5f 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1336,7 +1336,16 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const sensitivityAtEntry = contextResolutions; mapTypeDepth++; try { - const mapped = mapTypeInner(type, ctx); + let mapped: IrType | null; + try { + mapped = mapTypeInner(type, ctx); + } catch (e) { + // typescript-go's API can panic serializing a union's members (an + // empty tuple cloned as an array literal, `s.match(re) || []`): a + // union the checker cannot describe has no representation. + if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e; + mapped = null; + } if ( mapped !== null && memoKey !== null && diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json new file mode 100644 index 000000000..75792b997 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6187-js-match-or-empty-array.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6187-js-match-or-empty-array.js" + ], + "diags": [] +} diff --git a/tests/corpus/6187-js-match-or-empty-array.js b/tests/corpus/6187-js-match-or-empty-array.js new file mode 100644 index 000000000..2cce0e17d --- /dev/null +++ b/tests/corpus/6187-js-match-or-empty-array.js @@ -0,0 +1,20 @@ +// @deferred-fences: 1 +// `s.match(re) || []` types the empty array as an empty tuple that the +// typescript-go API panics to describe (moment.js's configFromStringAndFormat). +// The panic stays with the statements whose lowering asks about that +// type, so the rest of the program compiles and runs. + +/** @param {string} s */ +function firstDigit(s) { + return (s.match(/\d/) || [])[0]; +} + +/** @param {string} s */ +function digitCount(s) { + let count = 0; + for (const ch of s) if (ch >= '0' && ch <= '9') count++; + return count; +} + +const parsers = { firstDigit, digitCount }; +console.log(typeof parsers.firstDigit, parsers.digitCount('a1b22')); diff --git a/tests/diagnostics/checker-panic-tuple.ts b/tests/diagnostics/checker-panic-tuple.ts index fd6eacb8c..cf59bc1f0 100644 --- a/tests/diagnostics/checker-panic-tuple.ts +++ b/tests/diagnostics/checker-panic-tuple.ts @@ -1,5 +1,6 @@ // `''.match(/x/) || []` drives tsgo into the TupleType interface-conversion -// panic (upstream signature 01/13): the panic fence turns it into the -// source-anchored SC0004 — a failed compile, never a crashed CLI. +// panic (upstream signature 01/13) whenever the union's members are +// serialized: the union stays unmappable, so the statement reports its own +// refusals — a failed compile, never a crashed CLI. const a = ''.match(/x/) || []; console.log(a.length); diff --git a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt b/tests/harness/__snapshots__/checker-panic-tuple.ts.txt index 46d22bcd0..3927e5518 100644 --- a/tests/harness/__snapshots__/checker-panic-tuple.ts.txt +++ b/tests/harness/__snapshots__/checker-panic-tuple.ts.txt @@ -1,13 +1,13 @@ -checker-panic-tuple.ts:4:1 - error SC0004: the TypeScript checker crashed answering a query here (an upstream typescript-go bug): panic: interface conversion: checker.TypeData is *checker.TypeReference, not *checker.TupleType +checker-panic-tuple.ts:5:11 - error SC1090: logical operators on union-typed values outside conditions (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet - 3 | // source-anchored SC0004 — a failed compile, never a crashed CLI. - 4 | const a = ''.match(/x/) || []; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 5 | console.log(a.length); + 4 | // refusals — a failed compile, never a crashed CLI. + 5 | const a = ''.match(/x/) || []; + | ^~~~~~~~~~~~~~~~~~~ + 6 | console.log(a.length); -checker-panic-tuple.ts:5:13 - error SC1090: reading 'length' on a union-typed value (every arm must be an object/record with a same-typed field 'length'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet +checker-panic-tuple.ts:6:13 - error SC1090: reading 'length' on a union-typed value (every arm must be an object/record with a same-typed field 'length'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet - 4 | const a = ''.match(/x/) || []; - 5 | console.log(a.length); + 5 | const a = ''.match(/x/) || []; + 6 | console.log(a.length); | ^~~~~~~~ - 6 | \ No newline at end of file + 7 | \ No newline at end of file From 8a204b8e0f06f3162b225552438c10aada14e26b Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:57:19 -0700 Subject: [PATCH 13/67] Dispatch methods on class instances passed to untyped JS callbacks `list.map((rec) => rec.match(x))` over an array of class instances in a JavaScript file lowers the callback with a checked-dynamic parameter and checks the boxed closure into the native callback type. The instances cross into the dynamic tier only when the callback is called, inside that function boundary, so the native-capsule dispatch never learned their classes and `rec.match` threw "rec.match is not a function". One class or several, inherited methods or own, all failed (chardet's recogniser list, any `.map`/`.filter`/`.forEach` with an untyped arrow). A dynamic value checked into a function type now registers the classes its parameters carry, as a dynFrom of those values already does. Co-Authored-By: Claude Opus 5.5 --- .../lowering/class-dynamic-dispatch.ts | 6 +++ .../6188-js-callback-class-instances.js.json | 6 +++ .../6188-js-callback-class-instances.js | 39 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json create mode 100644 tests/corpus/6188-js-callback-class-instances.js diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 120032bf5..a0ed496e1 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -830,6 +830,12 @@ export class ClassDynamicDispatch { break; case "dynCheck": if (isDynTypedRefType(expr.type)) rewrite.add(fn); + // A dynamic function checked into a native function type + // receives its arguments as dynamic values: class instances + // passed to it cross the boundary when it is called (a JS + // `list.map((rec) => rec.match(x))` callback with an untyped + // parameter). + if (expr.type.kind === "func") expr.type.params.forEach(discover); break; case "libCall": if ( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json new file mode 100644 index 000000000..baf651d61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6188-js-callback-class-instances.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6188-js-callback-class-instances.js" + ], + "diags": [] +} diff --git a/tests/corpus/6188-js-callback-class-instances.js b/tests/corpus/6188-js-callback-class-instances.js new file mode 100644 index 000000000..22df40f9b --- /dev/null +++ b/tests/corpus/6188-js-callback-class-instances.js @@ -0,0 +1,39 @@ +// Class instances passed to an untyped JavaScript callback (chardet's +// `recognisers.map((rec) => rec.match(context))`): the callback reads its +// parameter as a dynamic value, and method calls on it reach the +// instances' compiled methods, inherited ones included. + +class Recogniser { + name() { + return 'base'; + } + match(input) { + return input.length > 2 ? `${this.name()}:${input.length}` : null; + } +} +class ShiftJis extends Recogniser { + name() { + return 'Shift_JIS'; + } +} +class Big5 extends Recogniser { + name() { + return 'Big5'; + } + match(input) { + return `big5 ${super.match(input)}`; + } +} +class Utf8 { + match(input) { + return `utf8:${input}`; + } +} + +const one = [new ShiftJis(), new ShiftJis()]; +console.log(one.map((rec) => rec.match('abc')).join(' ')); + +const mixed = [new ShiftJis(), new Big5(), new Utf8()]; +console.log(JSON.stringify(mixed.map((rec) => rec.match('abcd')))); +console.log(mixed.filter((rec) => rec.match('ab') !== null).length); +mixed.forEach((rec, i) => console.log(i, rec.match('xyz'))); From 29909ed1aa6011ece16726cc26a23d62f98fd645 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:57:20 -0700 Subject: [PATCH 14/67] Give tsc's void-init export preamble no storage for class exports tsc opens every CommonJS file with `exports.A = exports.B = void 0;` and later aliases `exports.A = A;`. Collection registered an export global for the chain's first name at the preamble, while the preamble lowered to nothing and the class alias registered and stored nothing. Importers then read `lib.A` from a global that was never assigned, and `new lib.A()` crashed with a segfault (chardet's encoding modules). The preamble detection moves to program.ts so collection and lowering share it. A dropped preamble whose real assignment is a class alias now registers no storage, so importers resolve to the class. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-modules.ts | 6 ++ .../src/frontend/lowering/lower-stmts.ts | 43 +------------ packages/compiler/src/frontend/program.ts | 61 +++++++++++++++++++ .../main.js.json | 7 +++ .../lib.js | 25 ++++++++ .../main.js | 9 +++ .../package.json | 1 + 7 files changed, 111 insertions(+), 41 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/lib.js create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/main.js create mode 100644 tests/corpus/6189-cjs-void-preamble-class-exports/package.json diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index df6677d5b..9f651925b 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,6 +18,7 @@ import { isRelativeSpecifier } from "../workspace-registry.js"; import { canonicalBuiltinModule, cjsExportAssignmentOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, entryPackageFilePredicate, isCjsJsFile, @@ -1398,6 +1399,11 @@ export function collectGlobals( // symbol (cjsMemberExportClassSymbol) and the class registry // applies unchanged. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue; + // A dropped tsc void-init preamble whose real assignment is such a + // class alias registers nothing either: storage here would never + // be assigned, and importers would read it instead of the class. + const real = cjsVoidInitPreambleReals(cjs)?.get(cjs.name.text); + if (real && lowerer.cjsMemberExportClassSymbol(real.expr) !== null) continue; registerExport(cjs.name, cjs.name.text, cjs.value); continue; } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 65f39c6ab..40dc3ccae 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -60,6 +60,7 @@ import { import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -6006,47 +6007,7 @@ function lowerCjsExportStatement( // the real assignment's position already orders. Preambles with no // later real assignment keep the ordinary path (the export IS // undefined then). - if (ts.isIdentifier(cjs.name)) { - const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression => - ts.isPropertyAccessExpression(e) && - ts.isIdentifier(e.name) && - ((ts.isIdentifier(e.expression) && e.expression.text === "exports") || - (ts.isPropertyAccessExpression(e.expression) && - ts.isIdentifier(e.expression.expression) && - e.expression.expression.text === "module" && - ts.isIdentifier(e.expression.name) && - e.expression.name.text === "exports")); - const isVoid = (e: ts.Expression): boolean => { - while (ts.isParenthesizedExpression(e)) e = e.expression; - return ( - e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined") - ); - }; - // chains: `exports.a = exports.b = void 0;` collects every name - const names: string[] = [cjs.name.text]; - let tail: ts.Expression = cjs.value; - while ( - ts.isBinaryExpression(tail) && - tail.operatorToken.kind === ts.SyntaxKind.EqualsToken && - isExportsMember(tail.left) - ) { - names.push((tail.left.name as ts.Identifier).text); - tail = tail.right; - } - if (isVoid(tail)) { - const sfHere = cjs.expr.getSourceFile(); - const myPos = cjs.expr.getStart(sfHere); - const laterRealOf = (name: string): boolean => - sfHere.statements.some((s) => { - const other = cjsExportAssignmentOf(s); - if (other?.kind !== "member" || !ts.isIdentifier(other.name)) return false; - if (other.name.text !== name) return false; - if (other.expr.getStart(sfHere) <= myPos) return false; - return !isVoid(other.value); - }); - if (names.every(laterRealOf)) return { kind: "block", body: [], loc }; - } - } + if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; const sf = cjs.expr.getSourceFile(); let root: IrGlobal | undefined; for (const stmt of sf.statements) { diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index df0cb673e..0deb5261d 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -5830,6 +5830,67 @@ export function cjsExportAssignmentOf( return null; } +/** The tsc VOID-INIT PREAMBLE (`exports.a = exports.b = void 0;` with the + * real `exports.a = a;` further down; every tsc-emitted dist file opens its + * exports this way): when every name in the chain has a later real + * assignment, the export takes its value there and the preamble lowers to + * nothing. Returns those later assignments by name, or null when this is + * not such a preamble (a preamble with no later real assignment keeps the + * ordinary path: the export IS undefined then). */ +export function cjsVoidInitPreambleReals( + cjs: Extract, { kind: "member" }>, +): Map, { kind: "member" }>> | null { + if (!ts.isIdentifier(cjs.name)) return null; + const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression => + ts.isPropertyAccessExpression(e) && + ts.isIdentifier(e.name) && + ((ts.isIdentifier(e.expression) && e.expression.text === "exports") || + (ts.isPropertyAccessExpression(e.expression) && + ts.isIdentifier(e.expression.expression) && + e.expression.expression.text === "module" && + ts.isIdentifier(e.expression.name) && + e.expression.name.text === "exports")); + const isVoid = (e: ts.Expression): boolean => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ( + e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined") + ); + }; + // chains: `exports.a = exports.b = void 0;` collects every name + const names: string[] = [cjs.name.text]; + let tail: ts.Expression = cjs.value; + while ( + ts.isBinaryExpression(tail) && + tail.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isExportsMember(tail.left) + ) { + names.push((tail.left.name as ts.Identifier).text); + tail = tail.right; + } + if (!isVoid(tail)) return null; + const sf = cjs.expr.getSourceFile(); + const myPos = cjs.expr.getStart(sf); + const reals = new Map< + string, + Extract, { kind: "member" }> + >(); + for (const name of names) { + const real = sf.statements + .map((s) => cjsExportAssignmentOf(s)) + .find( + (other) => + other?.kind === "member" && + ts.isIdentifier(other.name) && + other.name.text === name && + other.expr.getStart(sf) > myPos && + !isVoid(other.value), + ); + if (real?.kind !== "member") return null; + reals.set(name, real); + } + return reals; +} + /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json new file mode 100644 index 000000000..3e7347489 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6189-cjs-void-preamble-class-exports/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js", + "/tests/corpus/6189-cjs-void-preamble-class-exports/main.js" + ], + "diags": [] +} diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js b/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js new file mode 100644 index 000000000..dfbfcc30e --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/lib.js @@ -0,0 +1,25 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Big5 = exports.ShiftJis = exports.describe = void 0; +class Recogniser { + name() { + return 'base'; + } +} +class ShiftJis extends Recogniser { + name() { + return 'Shift_JIS'; + } +} +exports.ShiftJis = ShiftJis; +class Big5 extends Recogniser { + name() { + return 'Big5'; + } +} +exports.Big5 = Big5; +/** @param {Recogniser} rec */ +function describe(rec) { + return `recogniser ${rec.name()}`; +} +exports.describe = describe; diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/main.js b/tests/corpus/6189-cjs-void-preamble-class-exports/main.js new file mode 100644 index 000000000..385bc7a4f --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/main.js @@ -0,0 +1,9 @@ +// tsc's CommonJS export preamble (`exports.A = exports.B = void 0;`) ahead +// of class exports (chardet's encoding modules): the class read through +// the module object is the class itself, for every name in the chain. +"use strict"; +const lib = require("./lib.js"); + +const list = [new lib.ShiftJis(), new lib.Big5()]; +for (const rec of list) console.log(lib.describe(rec), rec instanceof lib.ShiftJis); +console.log(typeof lib.Big5, new lib.Big5().name()); diff --git a/tests/corpus/6189-cjs-void-preamble-class-exports/package.json b/tests/corpus/6189-cjs-void-preamble-class-exports/package.json new file mode 100644 index 000000000..a3c15a7a6 --- /dev/null +++ b/tests/corpus/6189-cjs-void-preamble-class-exports/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } From 77b137268d528b2c7d88b7038c9287b8e9959283 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:13 -0700 Subject: [PATCH 15/67] Visit each Set-Cookie value separately in Headers#forEach Fetch's "sort and combine" never combines Set-Cookie: Node's Headers iterator yields one entry per Set-Cookie value, in insertion order, while get() still joins them with ", ". The native forEach walked the sorted unique names and read the combined value, so repeated cookies reached the callback as one joined string. forEach now indexes the expanded list per step (sf_headers_sorted_entry), keeping the live re-sort so callback mutations shift later entries as in Node. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 63 +++++++++++++++---- .../fetch-headers-set-cookie-iteration.ts | 63 +++++++++++++++++++ 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 tests/corpus/fetch-headers-set-cookie-iteration.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index f586b96f8..dc53c7f22 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2386,6 +2386,50 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) { return names; } +static bool sf_headers_is_set_cookie(const ScrStr *name) { + return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0; +} + +/* The `index`th entry of Fetch's "sort and combine" list: names in + * byte order, each with its combined value, except that every set-cookie + * value is its own entry (in insertion order), as Node's Headers + * iterator yields them. False past the end. Outputs are +1. */ +static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, + ScrStr **name_out, + ScrStr **value_out) { + size_t count = 0; + ScrStr **names = sf_headers_sorted_names(h, &count); + bool found = false; + size_t at = 0; + for (size_t i = 0; i < count && !found; i++) { + if (!sf_headers_is_set_cookie(names[i])) { + if (at == index) { + *name_out = scr_str_retain(names[i]); + *value_out = sf_headers_get_value(h, names[i]); + found = true; + } + at++; + continue; + } + size_t n = (size_t)scr_arr_len(h->pairs); + for (size_t j = 0; j + 1 < n && !found; j += 2) { + ScrStr *key = scr_arr_get_ref(h->pairs, (double)j); + bool match = sf_headers_is_set_cookie(key); + scr_str_release(key); + if (!match) continue; + if (at == index) { + *name_out = scr_str_retain(names[i]); + *value_out = scr_arr_get_ref(h->pairs, (double)(j + 1)); + found = true; + } + at++; + } + } + for (size_t j = 0; j < count; j++) scr_str_release(names[j]); + free(names); + return found; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, @@ -2438,20 +2482,15 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, * a future name skips it, inserting a later name visits it, and * inserting before the current index can make the current name appear * again. The old one-time snapshot was only safe while every native - * Headers handle was immutable. */ + * Headers handle was immutable. Set-Cookie values are visited one by + * one (sf_headers_sorted_entry). */ for (size_t i = 0;; i++) { - size_t count = 0; - ScrStr **names = sf_headers_sorted_names(h, &count); - if (i >= count) { - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); - break; - } - ScrStr *value = sf_headers_get_value(h, names[i]); + ScrStr *name = NULL; + ScrStr *value = NULL; + if (!sf_headers_sorted_entry(h, i, &name, &value)) break; ScrDyn *boxed_value = scr_dyn_new_str(value); - ScrDyn *boxed_name = scr_dyn_new_str(names[i]); - for (size_t j = 0; j < count; j++) scr_str_release(names[j]); - free(names); + ScrDyn *boxed_name = scr_dyn_new_str(name); + scr_str_release(name); ScrDyn *call_args[3] = {boxed_value, boxed_name, self}; scr_dyn_this_push_dyn(this_arg); ScrDyn *result = diff --git a/tests/corpus/fetch-headers-set-cookie-iteration.ts b/tests/corpus/fetch-headers-set-cookie-iteration.ts new file mode 100644 index 000000000..4d39bab70 --- /dev/null +++ b/tests/corpus/fetch-headers-set-cookie-iteration.ts @@ -0,0 +1,63 @@ +// Headers iteration follows Fetch's "sort and combine": names in byte +// order with their combined values, except that every Set-Cookie value is +// visited on its own. get() still joins them; getSetCookie() lists them. +const h = new Headers(); +h.append("set-cookie", "a=1; Path=/"); +h.append("x-b", "1"); +h.append("Set-Cookie", "b=2, c=3"); +h.append("x-a", "1"); +h.append("x-a", "2"); +h.append("set-cookie", "d=4"); +h.forEach((value, name) => console.log(`${name}: ${value}`)); +console.log(JSON.stringify(h.get("set-cookie"))); +console.log(JSON.stringify(h.getSetCookie())); + +// A thisArg and the Headers object reach the callback. +const self = { tag: "this" }; +h.forEach(function (this: typeof self, _value, name, headers) { + if (name === "x-b") console.log(this.tag, headers === h); +}, self); + +// Iteration is live and indexes the expanded list: deleting the cookies +// while visiting the first one shifts the next name into its slot. +const live = new Headers(); +live.append("set-cookie", "a=1"); +live.append("set-cookie", "b=2"); +live.append("x-z", "z"); +const seen: string[] = []; +live.forEach((value, name) => { + seen.push(`${name}=${value}`); + if (value === "a=1") live.delete("set-cookie"); +}); +console.log(seen.join(" | ")); + +// Appending a cookie during the walk is visited when it sorts later. +const grow = new Headers(); +grow.append("set-cookie", "a=1"); +grow.append("x-y", "y"); +const grown: string[] = []; +grow.forEach((value, name) => { + grown.push(`${name}=${value}`); + if (value === "a=1") grow.append("set-cookie", "b=2"); +}); +console.log(grown.join(" | ")); + +// Only set-cookie stays separate; cookie and other repeats combine. +const plain = new Headers([ + ["cookie", "a=1"], + ["cookie", "b=2"], + ["accept", "x"], + ["accept", "y"], +]); +plain.forEach((value, name) => console.log(`${name}: ${value}`)); +console.log(JSON.stringify(plain.getSetCookie())); + +// A response's headers walk the same way. +const response = new Response("ok", { + headers: [ + ["set-cookie", "s=1"], + ["set-cookie", "t=2"], + ["content-type", "text/plain"], + ], +}); +response.headers.forEach((value, name) => console.log(`${name}: ${value}`)); From 46d238fc0a70613d011ec5fb5eefd7b17fb92371 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:21 -0700 Subject: [PATCH 16/67] Send repeated Set-Cookie request headers as one combined line Headers stores each Set-Cookie value separately, but undici dispatches its header list's combined value: one line with the values joined by ", " at the position of the first one. fetch() wrote one line per value. The request serializer now joins later Set-Cookie values into the first entry and skips the rest, for every HeadersInit form. Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 27 +++++++++++- tests/corpus/fetch-request-set-cookie-wire.ts | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/fetch-request-set-cookie-wire.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index dc53c7f22..f5970ea0f 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4454,14 +4454,39 @@ static bool sf_start_hop(SfTransfer *t) { } if (connection_close) sf_push_header_text(headers, "connection", "close", 5); else sf_push_header_text(headers, "connection", "keep-alive", 10); + /* Headers keeps each Set-Cookie value separately, but undici sends its + * header list's combined value: one line, the values joined by ", ", + * at the position of the first one. */ + bool set_cookie_sent = false; for (size_t i = 0; i + 1 < user_len; i += 2) { ScrStr *name = scr_arr_get_ref(t->headers, (double)i); ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1)); - if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection")) { + if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection") || + (set_cookie_sent && sf_eq_ci(name, "set-cookie"))) { scr_str_release(name); scr_str_release(value); continue; } + if (sf_eq_ci(name, "set-cookie")) { + set_cookie_sent = true; + for (size_t j = i + 2; j + 1 < user_len; j += 2) { + ScrStr *later = scr_arr_get_ref(t->headers, (double)j); + bool match = sf_eq_ci(later, "set-cookie"); + scr_str_release(later); + if (!match) continue; + ScrStr *next = scr_arr_get_ref(t->headers, (double)(j + 1)); + size_t joined_len = value->len + 2 + next->len; + char *joined = malloc(joined_len ? joined_len : 1); + if (!joined) sf_oom(); + memcpy(joined, value->data, value->len); + memcpy(joined + value->len, ", ", 2); + memcpy(joined + value->len + 2, next->data, next->len); + scr_str_release(next); + scr_str_release(value); + value = scr_str_new(joined, joined_len); + free(joined); + } + } scr_arr_push_ref(headers, name); scr_arr_push_ref(headers, value); } diff --git a/tests/corpus/fetch-request-set-cookie-wire.ts b/tests/corpus/fetch-request-set-cookie-wire.ts new file mode 100644 index 000000000..54789a3fb --- /dev/null +++ b/tests/corpus/fetch-request-set-cookie-wire.ts @@ -0,0 +1,44 @@ +// fetch() sends its header list's combined values: repeated Set-Cookie +// request headers go out as one line, the values joined by ", ", at the +// position of the first one, whatever form the headers were given in. +import { createServer } from "node:http"; + +function shown(raw: string[]): string { + const lines: string[] = []; + for (let i = 0; i < raw.length; i += 2) { + const name = raw[i]!; + const lower = name.toLowerCase(); + if (lower === "host" || lower === "connection" || lower.startsWith("accept") || lower.startsWith("sec-") || lower === "user-agent") continue; + lines.push(`${name}: ${raw[i + 1]}`); + } + return lines.join(" | "); +} + +const server = createServer((req, res) => { + res.end(shown(req.rawHeaders)); +}); + +server.listen(0, "127.0.0.1", async () => { + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + const url = `http://127.0.0.1:${port}/`; + + const headers = new Headers(); + headers.append("set-cookie", "a=1"); + headers.append("x-first", "1"); + headers.append("set-cookie", "b=2"); + console.log(await (await fetch(url, { headers })).text()); + + const pairs: [string, string][] = [ + ["x-before", "0"], + ["set-cookie", "c=3"], + ["set-cookie", "d=4; Path=/"], + ]; + console.log(await (await fetch(url, { headers: pairs })).text()); + + console.log(await (await fetch(url, { headers: { "set-cookie": "e=5", "x-record": "r" } })).text()); + + const single = new Headers({ "set-cookie": "only=1" }); + console.log(await (await fetch(url, { headers: single })).text()); + server.close(); +}); From 2957a9c042f6f51e0f90910b068e50a51fd4d072 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:21 -0700 Subject: [PATCH 17/67] Report unparsable fetch and Request URLs as "Failed to parse URL from" undici's Request constructor wraps the URL parser's failure: TypeError "Failed to parse URL from " with the parser's TypeError as its non-enumerable cause. fetch() rejected, and new Request() threw, with the bare "Invalid URL" error instead. Both paths now replace the pending parser error with the wrapping TypeError (sf_throw_url_parse_failure). Co-Authored-By: Claude Opus 5.5 --- packages/runtime/src/scr_fetch.c | 27 ++++++++++++++++ tests/corpus/fetch-invalid-url-cause.ts | 41 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/corpus/fetch-invalid-url-cause.ts diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index f5970ea0f..7aba5a4b2 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -312,6 +312,31 @@ static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) { return d; } +/* Request's URL parse failure: with the URL parser's TypeError pending, + * replace it by undici's TypeError "Failed to parse URL from " + * whose (non-enumerable) cause is the parser error. */ +static void sf_throw_url_parse_failure(const ScrStr *input) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *cause = scr_caught_to_dyn(caught); + scr_caught_release(caught); + /* Sized without the NUL so it stays out of the unit's shared mergeable + * string section (which every program linking this unit carries). */ + static const char prefix[25] = "Failed to parse URL from "; + size_t message_len = sizeof prefix + input->len; + char *message = malloc(message_len); + if (!message) sf_oom(); + memcpy(message, prefix, sizeof prefix); + memcpy(message + sizeof prefix, input->data, input->len); + ScrStr *text = scr_str_new(message, message_len); + free(message); + ScrError *e = scr_error_new(SCR_ERR_TYPE, text); + scr_str_release(text); + scr_error_set_cause(e, cause); + e->cause_enumerable = false; + scr_dyn_release(cause); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + /* ── AbortSignal ─────────────────────────────────────────────────── */ static void sf_signal_track_reason(SfSignal *s) { @@ -4873,6 +4898,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) { ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input); if (!url_text) return NULL; ScrUrl *url = scr_url_new(url_text); + if (!url) sf_throw_url_parse_failure(url_text); scr_str_release(url_text); if (!url) return NULL; if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; } @@ -5182,6 +5208,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) { ScrUrl *u = scr_url_new(url); if (!u) { + sf_throw_url_parse_failure(url); scr_dyn_release(coerced_body); scr_arr_release(headers); scr_str_release(method); diff --git a/tests/corpus/fetch-invalid-url-cause.ts b/tests/corpus/fetch-invalid-url-cause.ts new file mode 100644 index 000000000..5517c4ae2 --- /dev/null +++ b/tests/corpus/fetch-invalid-url-cause.ts @@ -0,0 +1,41 @@ +// An unparsable fetch() or Request URL is undici's TypeError +// "Failed to parse URL from ", whose non-enumerable cause is the +// URL parser's TypeError. +function report(label: string, e: unknown): void { + const serialized = JSON.stringify(e); + if (!(e instanceof Error)) { + console.log(label, "not an Error"); + return; + } + console.log(label, e.name, e.message, e instanceof TypeError); + console.log(" has cause:", "cause" in e, "own enumerable:", serialized); + console.log( + " cause:", + e.cause instanceof Error ? `${e.cause.name} ${e.cause.message} ${e.cause instanceof TypeError}` : String(e.cause), + ); +} + +async function main(): Promise { + for (const input of ["http://127.0.0.1:1http://x/", "/relative/path", "", "http://[::1"]) { + try { + await fetch(input); + console.log("resolved?"); + } catch (e) { + report(`fetch(${JSON.stringify(input)})`, e); + } + } + try { + new Request("not a url"); + console.log("constructed?"); + } catch (e) { + report("new Request", e); + } + // A valid URL with credentials keeps its own message. + try { + await fetch("http://user:pass@127.0.0.1:1/"); + } catch (e) { + report("credentials", e); + } +} + +void main(); From 43cefeb56b7c559507185239296f38fe006c7815 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:28 -0700 Subject: [PATCH 18/67] Retag flow-narrowed unions before primitive toString() `headers.host.toString()` after `!Array.isArray(headers.host)` narrows a `string | number | string[]` slot to `string | number`, and the union toString lowering accepted the narrowed checker type but emitted the toString node over the wider storage union. The validator rejected the non-stringable arm as an internal compiler error (SC9001). The lowering now retags the read to the proven union (the same checked coercion other narrowed reads use) before the per-union ToString helper. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-calls.ts | 9 ++++++- tests/corpus/union-narrowed-tostring.ts | 27 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/union-narrowed-tostring.ts diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c..cd96f2aae 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -4210,8 +4210,15 @@ function lowerUnionToStringCall( (a.kind === "bytes" && a.elem === "u8"), ); if (!stringable) return null; - const operand = lowerer.lowerExpr(access.expression); + let operand = lowerer.lowerExpr(access.expression); if (operand.type.kind !== "union") return null; + // A flow-narrowed read keeps its wider storage union (`string | number` + // after `!Array.isArray(h.host)` over a `string | number | string[]` + // slot): retag it to the proven union before the per-union helper. + if (operand.type.unionId !== recvT.unionId) { + operand = lowerer.coerceInto(access.expression, operand, recvT); + if (operand.type.kind !== "union" || operand.type.unionId !== recvT.unionId) return null; + } return { kind: "toString", operand, type: STRING, loc: locOf(call) }; } diff --git a/tests/corpus/union-narrowed-tostring.ts b/tests/corpus/union-narrowed-tostring.ts new file mode 100644 index 000000000..93660b332 --- /dev/null +++ b/tests/corpus/union-narrowed-tostring.ts @@ -0,0 +1,27 @@ +// .toString() on a flow-narrowed union whose storage union is wider: the +// read retags to the proven arms, then formats like String(). +import type { OutgoingHttpHeaders } from "node:http"; + +function host(headers?: OutgoingHttpHeaders): string { + if (headers?.host && !Array.isArray(headers.host)) return headers.host.toString(); + return ""; +} +console.log(JSON.stringify([host({ host: "a:1" }), host({ host: 3 }), host({}), host()])); + +function label(v: string | number | string[] | undefined): string { + if (v === undefined) return "none"; + if (Array.isArray(v)) return `list(${v.length})`; + return v.toString(); +} +console.log(label("x"), label(1.5), label(-0), label(["a", "b"]), label(undefined)); + +function flag(v: boolean | number | null): string { + return v === null ? "null" : v.toString(); +} +console.log(flag(true), flag(false), flag(1e21), flag(null)); + +type Field = string | number | { nested: string }; +function field(f: Field): string { + return typeof f === "object" ? f.nested : f.toString(); +} +console.log(field("s"), field(42), field({ nested: "n" })); From def4626396c81e789fe903fc6b803c4cb830914a Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:15:28 -0700 Subject: [PATCH 19/67] Lower optional calls of always-present library methods as plain calls `h.getSetCookie?.()` on a native Headers handle failed with SC2020: the optional-chain lowering first reads the callee as a value, and native handle methods have no first-class value. A required standard-library method on a never-nullish receiver always exists, so `?.()` is the plain call. When that method read has no lowering, the optional chain now re-dispatches the plain call. Reads that lower keep the existing path unchanged. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/optional-chains.ts | 47 ++++++++++++++++++- tests/corpus/optional-call-native-method.ts | 28 +++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/optional-call-native-method.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 7da847062..51afc01ab 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -4,6 +4,8 @@ import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../.. import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js"; import { isNodeEsmFile, locOf } from "../../program.js"; import type { Lowerer } from "../lowerer.js"; +import { tryLowerExpression } from "./try-lower-expression.js"; +import { constituentTypes } from "../../ts7/checker.js"; /** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on * the member access) and `f?.()` (the token on the call). The receiver @@ -121,6 +123,31 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean { } } +/** `recv.m` where `m` is a required method the standard library declares + * and `recv` itself is a non-optional, never-nullish typed receiver. */ +function alwaysPresentStdlibMethod(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isPropertyAccessExpression(node) || node.questionDotToken) return false; + if (hasOptionalChainGuard(node.expression)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(node.name); + if (!symbol || !(symbol.flags & ts.SymbolFlags.Method) || symbol.flags & ts.SymbolFlags.Optional) + return false; + if (!lowerer.isStdlibSymbol(symbol)) return false; + const nullable = + ts.TypeFlags.Any | + ts.TypeFlags.Unknown | + ts.TypeFlags.Undefined | + ts.TypeFlags.Null | + ts.TypeFlags.Void; + const receiverType = lowerer.checker.getTypeAtLocation(node.expression); + const calleeType = lowerer.checker.getTypeAtLocation(node); + const parts = (type: ts.Type): readonly ts.Type[] => + type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type]; + return ( + parts(receiverType).every((part) => !(part.flags & nullable)) && + parts(calleeType).every((part) => !(part.flags & nullable)) + ); +} + export function lowerOptionalChain( lowerer: Lowerer, expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression, @@ -167,7 +194,25 @@ export function lowerOptionalChain( dotNode = tail; recvNode = tail.expression; } - const loweredReceiver = lowerer.lowerExpr(recvNode); + // `h.getSetCookie?.()`: a required standard-library METHOD on a + // never-nullish receiver always exists, so `?.()` is the plain call. + // Native handle methods have no first-class value to guard; when the + // method read itself has no lowering, re-dispatch the plain call (which + // reports its own diagnostics) instead of failing on the value read. + let loweredReceiver: IrExpr | null; + if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) { + loweredReceiver = tryLowerExpression(lowerer, recvNode); + if (loweredReceiver === null) { + lowerer.chainHandled.add(dotNode); + try { + return lowerer.lowerExpr(expr); + } finally { + lowerer.chainHandled.delete(dotNode); + } + } + } else { + loweredReceiver = lowerer.lowerExpr(recvNode); + } const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { diff --git a/tests/corpus/optional-call-native-method.ts b/tests/corpus/optional-call-native-method.ts new file mode 100644 index 000000000..e3b9aa4a7 --- /dev/null +++ b/tests/corpus/optional-call-native-method.ts @@ -0,0 +1,28 @@ +// An optional call of a standard-library method that always exists on a +// never-nullish receiver is the plain call (`getSetCookie?.()` is always +// present on Node 24). +const h = new Headers(); +h.append("set-cookie", "a=1"); +h.append("set-cookie", "b=2"); +const cookies = h.getSetCookie?.() ?? []; +console.log(JSON.stringify(cookies)); +console.log(h.get?.("set-cookie")); +console.log(h.has?.("x-missing")); +h.set?.("x-set", "1"); +h.forEach?.((value, name) => console.log(`${name}=${value}`)); + +// The receiver evaluates once; arguments evaluate after it. +let reads = 0; +function headers(): Headers { + reads++; + return h; +} +function arg(): string { + console.log("argument after receiver", reads); + return "x-set"; +} +console.log(headers().get?.(arg()), reads); + +// Strings and arrays: the same rule. +const s = "Hello"; +console.log(s.toUpperCase?.(), [3, 1, 2].sort?.().join(",")); From 4d3dde55a72992470925d71fac1d17a21668b1e6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:17:48 -0700 Subject: [PATCH 20/67] Return undefined from JSON.stringify of unknown roots where it is observed JSON.stringify answers undefined for an undefined, function, or symbol root. Through `unknown` (or a generic instantiated with it) the dyn-root lowering converted the runtime result to a string, so the result was the text "undefined": `text === undefined` was false and `typeof text` was "string". Where the result reaches a use that distinguishes undefined from that text (undefined/null comparisons, typeof, ??, ?., truth tests, a return from a function declaring undefined, or an unannotated local binding used that way), the call now keeps the checked `string | undefined` result the replacer path already produces; later string uses of such a binding narrow by the checker's flow type. Other uses keep the existing string ABI, so programs that cannot observe the difference compile unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/json.ts | 109 +++++++++++++++++- .../json-stringify-unknown-undefined-root.ts | 38 ++++++ 2 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/corpus/json-stringify-unknown-undefined-root.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a73c9d5e7..7ce902772 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -15,6 +15,7 @@ import { isUnitType, } from "../../../ir/ir.js"; import { optionalStringTags } from "./arguments.js"; +import { constituentTypes } from "../../ts7/checker.js"; /** JSON parse produces checked-dynamic data; a native reviver walks it * bottom-up before any checked cast. Stringify without a callback keeps @@ -177,9 +178,11 @@ export function lowerJsonMethodCall( type: DYN, loc, }; - // Preserve the existing dyn-root string ABI, including its textual - // "undefined" result, while letting the runtime honor toJSON hooks. - return value.type.kind === "dyn" + // A dyn root can serialize to undefined (undefined, a function, a + // symbol). Where the program can observe that (jsonResultObserved), + // keep the actual undefined; elsewhere the dyn-root string ABI + // stays, whose undefined result prints the same text. + return value.type.kind === "dyn" && !jsonResultObserved(lowerer, call) ? { kind: "toString", operand: raw, type: STRING, loc } : { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc }; } @@ -276,6 +279,106 @@ function lowerOptionalStringifyRoot( return { kind: "call", callee: helper, args: [value], type: resultT, loc }; } +/** True when a `JSON.stringify` result is used where JavaScript can tell + * undefined from the text "undefined": compared with undefined/null, + * `typeof`, `??`, `?.`, truth tests, a return from a function declaring + * an undefined result, or an unannotated local binding whose references + * do one of these. The lib types say `string`, so other uses (concatenation, + * printing, string parameters) cannot tell the difference. */ +function jsonResultObserved(lowerer: Lowerer, call: ts.CallExpression): boolean { + let node: ts.Node = call; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + if (observesUndefined(node as ts.Expression)) return true; + const parent = node.parent; + if (ts.isReturnStatement(parent)) { + let fn: ts.Node | undefined = parent.parent; + while (fn && !ts.isFunctionLike(fn)) fn = fn.parent; + const annotation = fn && ts.isFunctionLike(fn) ? fn.type : undefined; + if (!annotation) return false; + const type = lowerer.checker.getTypeAtLocation(annotation); + const parts = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type]; + return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0); + } + // Module-scope bindings are globals whose slot type comes from the + // checker's `string`; only function- and block-scoped locals adopt the + // initializer's checked `string | undefined`. + if ( + !ts.isVariableDeclaration(parent) || + parent.initializer !== node || + parent.type || + !ts.isIdentifier(parent.name) || + (ts.isVariableDeclarationList(parent.parent) && + ts.isVariableStatement(parent.parent.parent) && + ts.isSourceFile(parent.parent.parent.parent)) + ) + return false; + const name = parent.name; + const symbol = lowerer.checker.getSymbolAtLocation(name); + if (!symbol) return false; + let scope: ts.Node = parent; + while (!ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent) && scope.parent) + scope = scope.parent; + let observed = false; + const visit = (current: ts.Node): void => { + if (observed) return; + if (ts.isIdentifier(current)) { + if ( + current !== name && + current.text === name.text && + lowerer.checker.getSymbolAtLocation(current) === symbol && + observesUndefined(current) + ) + observed = true; + return; + } + ts.forEachChild(current, visit); + }; + visit(scope.parent ?? scope); + return observed; +} + +function observesUndefined(expr: ts.Expression): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + if (ts.isTypeOfExpression(parent)) return true; + if (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) + return true; + if ( + (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && + parent.expression === node && + parent.questionDotToken + ) + return true; + if ( + (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) && + parent.expression === node + ) + return true; + if (ts.isConditionalExpression(parent) && parent.condition === node) return true; + if (ts.isBinaryExpression(parent)) { + const op = parent.operatorToken.kind; + if (op === ts.SyntaxKind.QuestionQuestionToken) return parent.left === node; + if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) + return parent.left === node; + if ( + op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.EqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken + ) { + let other: ts.Node = parent.left === node ? parent.right : parent.left; + while (ts.isParenthesizedExpression(other)) other = other.expression; + return ( + other.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(other) && other.text === "undefined") || + ts.isVoidExpression(other) + ); + } + } + return false; +} + /** Only explicit null/undefined select the no-callback path. */ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { if (ts.isParenthesizedExpression(node)) return jsonNullishArgument(lowerer, node.expression); diff --git a/tests/corpus/json-stringify-unknown-undefined-root.ts b/tests/corpus/json-stringify-unknown-undefined-root.ts new file mode 100644 index 000000000..52528afbf --- /dev/null +++ b/tests/corpus/json-stringify-unknown-undefined-root.ts @@ -0,0 +1,38 @@ +// JSON.stringify of an unknown (or generic) root whose value is undefined, +// a function, or a symbol returns undefined, not text. Wherever the program +// can tell the two apart (undefined/null comparisons, typeof, ??, ?., truth +// tests, returns declaring undefined, and bindings used that way) the +// native result is the actual undefined. Nested values follow Node's +// member rules: dropped from objects, null in arrays. +const u: unknown = undefined; +const f: unknown = () => 1; +const s: unknown = Symbol("x"); +const ok: unknown = { a: [1, undefined] }; +for (const v of [u, f, s, ok]) { + const t = JSON.stringify(v); + console.log(t === undefined, typeof t, t ?? "fb", t?.length, t == null, t !== undefined); +} +for (const v of [u, ok]) { + const t = JSON.stringify(v); + if (t) console.log("truthy", t.length); + else console.log("falsy"); + console.log(!t, t ? "y" : "n", t || "or", t && "and"); +} +console.log(JSON.stringify(u) === undefined, typeof JSON.stringify(f), JSON.stringify(s) ?? "fb2", JSON.stringify(u)?.length); +console.log(JSON.stringify(u) || "or2", JSON.stringify(ok) || "or3", !JSON.stringify(u)); +function raw(v: T): string | undefined { + return JSON.stringify(v); +} +console.log(typeof raw(u), raw(ok)); +function ser(v: T): string { + const text = JSON.stringify(v); + if (text === undefined) return "undefined result"; + return `string ${text} ${text.length}`; +} +console.log(ser(u), ser({ a: 1 }), ser(1), ser("s"), ser(ok)); +// Unobserved uses keep the plain string path. +const plain = JSON.stringify(ok); +console.log(plain.length, plain, `${JSON.stringify(u)}`, "x" + JSON.stringify(u)); +// A module-scope binding keeps the string slot; direct uses still observe. +const top = JSON.stringify(ok); +console.log(top.length, JSON.stringify(u) === undefined); From 58d3981476bf1ced130095cb4372c32cd64894f8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:20:14 -0700 Subject: [PATCH 21/67] Accept initializer-less symbol-keyed fields assigned in the constructor A computed TypeScript field without an initializer (`[INTERNALS]: T`) was refused even with a stable Symbol()/Symbol.for() or literal key, which contradicts the documented rule that TypeScript fields need an initializer or an unconditional constructor assignment. Such a field now takes its declared type and the identifier fields' definite-assignment rule: an unconditional top-level `this[key] = ...` in the constructor, otherwise the checked deferred-initialization slot. Types that admit undefined (optional fields) start undefined. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 44 ++++++++++++++++--- ...class-symbol-field-constructor-assigned.ts | 43 ++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/corpus/class-symbol-field-constructor-assigned.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index f3a4830c3..6a24b5417 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2644,9 +2644,15 @@ export function collectClassShapeInner( ? String(Number(member.name.text)) : member.name.text; const fieldName = symbolKey?.fieldName ?? stringKey; + // A TypeScript field without an initializer takes its declared + // type and the identifier fields' definite-assignment rule (an + // unconditional `this[key] = ...` at the constructor's top + // level), checked with the unguarded fields below. + const declaredOnly = + !member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type; if ( fieldName === null || - (!member.initializer && !isJsSourceFile(member.getSourceFile())) + (!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly) ) { lowerer.unsupported( "SC1090", @@ -2659,9 +2665,10 @@ export function collectClassShapeInner( member.type || !member.initializer ? declared : lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer)); - const mapped = member.initializer - ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) - : DYN; + const mapped = + member.initializer || declaredOnly + ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) + : DYN; let type = mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type ? DYN @@ -2681,7 +2688,24 @@ export function collectClassShapeInner( if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN; if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN; } - const undefinedInitializer = !member.initializer ? locOf(member) : undefined; + const undefinedInitializer = + !member.initializer && (!declaredOnly || type.kind === "dyn") + ? locOf(member) + : undefined; + if ( + declaredOnly && + type.kind !== "dyn" && + !( + type.kind === "union" && + lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT") + ) + ) { + unguardedFields.push({ + node: member.name, + name: fieldName, + why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`, + }); + } const previous = fields.get(fieldName); if (previous) { if (symbolKey && !symbolFields.has(symbolKey.identity)) { @@ -3588,6 +3612,16 @@ export function collectClassShapeInner( stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword ) { topAssigned.add(stmt.expression.left.name.text); + } else if ( + ts.isExpressionStatement(stmt) && + ts.isBinaryExpression(stmt.expression) && + stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isElementAccessExpression(stmt.expression.left) && + stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword + ) { + const key = stmt.expression.left.argumentExpression; + const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key); + if (name !== null) topAssigned.add(name); } } for (const f of unguardedFields) { diff --git a/tests/corpus/class-symbol-field-constructor-assigned.ts b/tests/corpus/class-symbol-field-constructor-assigned.ts new file mode 100644 index 000000000..55e6e4e4a --- /dev/null +++ b/tests/corpus/class-symbol-field-constructor-assigned.ts @@ -0,0 +1,43 @@ +// Symbol-keyed (and literal-keyed) TypeScript fields without an +// initializer take their declared type when the constructor assigns them +// unconditionally; an optional one starts undefined. A field assigned +// later (here from a method the constructor calls) reads through the +// checked deferred-initialization slot. +const Internal = Symbol("internal"); +const Registered = Symbol.for("pkg.registered"); +const Pending = Symbol("pending"); + +class Box { + [Internal]: { n: number }; + [Registered]: string[]; + ["literal"]: number; + [Pending]?: string[]; + constructor(n: number) { + this[Internal] = { n }; + this[Registered] = ["a"]; + this["literal"] = n * 2; + } + get n(): number { + return this[Internal].n; + } +} + +const b = new Box(5); +console.log(b.n, b[Registered].length, b.literal, b[Pending] === undefined); +b[Internal].n = 7; +b[Registered].push("b"); +b[Pending] = ["p"]; +console.log(b.n, b[Registered].join(","), b[Pending]?.join(",")); +console.log(JSON.stringify(Object.entries({ literal: b.literal }))); + +class Late { + [Internal]!: { n: number }; + constructor(flag: boolean) { + if (flag) this[Internal] = { n: 1 }; + else this.init(); + } + init(): void { + this[Internal] = { n: 2 }; + } +} +console.log(new Late(true)[Internal].n, new Late(false)[Internal].n); From 1fd405c03373a0422f06abb3218d3c5f48b71f05 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:25:55 -0700 Subject: [PATCH 22/67] Add Date.prototype.toUTCString `new Date(ms).toUTCString()` was refused (SC2020). It now lowers to a runtime formatter over the stored time value with V8's output: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a leading "-" and at least four digits for negative years, and "Invalid Date" for an invalid time value. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../src/frontend/lowering/builtins/dates.ts | 5 +- .../src/frontend/lowering/surfaces.ts | 7 ++ .../compiler/src/ir/builtin-signatures.ts | 3 + packages/runtime/src/scr_lib.c | 66 +++++++++++++++++++ packages/runtime/src/scr_number.h | 3 + tests/corpus/date-to-utc-string.ts | 26 ++++++++ tests/harness/library-profile.test.ts | 1 + 8 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/date-to-utc-string.ts diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02bf37192..d5aebf0d8 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -314,6 +314,7 @@ export const LIB_FN_SYMS: Record = { "date.getUTCDate": "scr_date_get_date_utc", "date.getDay": "scr_date_get_day_local", "date.getUTCDay": "scr_date_get_day_utc", + "date.toUTCString": "scr_date_to_utc_string", "date.getHours": "scr_date_get_hours_local", "date.getUTCHours": "scr_date_get_hours_utc", "date.getMinutes": "scr_date_get_minutes_local", diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index fa880bdc9..0101a3ec3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly> = { }; const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; + "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one * TimeClip'd millisecond scalar. Construction lives in lowerNew; @@ -113,6 +113,9 @@ export function lowerDateCall( if (name === "toISOString") { return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc }; } + if (name === "toUTCString") { + return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc }; + } const fn = own(DATE_GETTER_FNS, name); if (fn !== undefined) { return { kind: "libCall", fn, args: [receiver], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 73cc58c45..598fb6f9a 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2207,6 +2207,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["date.toISOString", "date.toISOStringValue"], note: "UTC ISO formatting over constructed and stored Date values", }, + { + id: "stdlib.date.toUTCString", + kind: "stdlib", + name: "Date.prototype.toUTCString", + fns: ["date.toUTCString"], + note: "RFC 7231 IMF-fixdate formatting over stored Date values", + }, { id: "stdlib.date.getFullYear", kind: "stdlib", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 08c493ae4..a3462c7d8 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4169,6 +4169,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.valueOf": { argTypes: [DATE_T], result: F64 }, "date.toISOString": { argTypes: [F64], result: STRING }, "date.toISOStringValue": { argTypes: [DATE_T], result: STRING }, + /** Date.prototype.toUTCString over a stored Date ("Invalid Date" for + * NaN; never throws). */ + "date.toUTCString": { argTypes: [DATE_T], result: STRING }, "date.getFullYear": { argTypes: [DATE_T], result: F64 }, "date.getUTCFullYear": { argTypes: [DATE_T], result: F64 }, "date.getMonth": { argTypes: [DATE_T], result: F64 }, diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index fd51e9c78..cd3f5d9a7 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10296,6 +10296,72 @@ ScrStr *scr_date_to_iso(double ms) { return scr_str_new(buf, (size_t)len); } +/* Node's Date.prototype.toUTCString over a stored (TimeClip'd) time + * value: "Www, DD Mmm YYYY HH:mm:ss GMT", a "-" and at least four digits + * for negative years, and "Invalid Date" for NaN. Never throws. +1. + * The text is assembled from function-local arrays rather than string + * literals: literals would land in this unit's shared mergeable string + * section and grow every executable, while these arrays are removed with + * the function by --gc-sections when a program never calls it. */ +ScrStr *scr_date_to_utc_string(double ms) { + /* Sized without the terminating NUL, so none is a C string the + * compiler could move into a mergeable string section. */ + static const char invalid[12] = "Invalid Date"; + static const char weekdays[21] = "SunMonTueWedThuFriSat"; + static const char months[36] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + if (!(fabs(ms) <= 8640000000000000.0)) return scr_str_new(invalid, sizeof invalid); + double t = trunc(ms); + double dayd = floor(t / 86400000.0); + long long msday = (long long)(t - dayd * 86400000.0); + long long days = (long long)dayd; + int weekday = (int)(((days + 4) % 7 + 7) % 7); + long long z = days + 719468; + long long era = (z >= 0 ? z : z - 146096) / 146097; + unsigned long long doe = (unsigned long long)(z - era * 146097); + unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + long long y = (long long)yoe + era * 400; + unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + unsigned long long mp = (5 * doy + 2) / 153; + unsigned long long d = doy - (153 * mp + 2) / 5 + 1; + unsigned long long m = mp < 10 ? mp + 3 : mp - 9; + if (m <= 2) y += 1; + int hh = (int)(msday / 3600000), mi = (int)(msday / 60000 % 60), + ss = (int)(msday / 1000 % 60); + char buf[40]; + size_t n = 0; + memcpy(buf, weekdays + 3 * weekday, 3); + n = 3; + buf[n++] = ','; + buf[n++] = ' '; + buf[n++] = (char)('0' + d / 10); + buf[n++] = (char)('0' + d % 10); + buf[n++] = ' '; + memcpy(buf + n, months + 3 * (m - 1), 3); + n += 3; + buf[n++] = ' '; + if (y < 0) buf[n++] = '-'; + unsigned long long year = (unsigned long long)(y < 0 ? -y : y); + char digits[20]; + size_t count = 0; + do { + digits[count++] = (char)('0' + year % 10); + year /= 10; + } while (year > 0); + while (count < 4) digits[count++] = '0'; + while (count > 0) buf[n++] = digits[--count]; + int parts[3] = {hh, mi, ss}; + for (int i = 0; i < 3; i++) { + buf[n++] = i == 0 ? ' ' : ':'; + buf[n++] = (char)('0' + parts[i] / 10); + buf[n++] = (char)('0' + parts[i] % 10); + } + buf[n++] = ' '; + buf[n++] = 'G'; + buf[n++] = 'M'; + buf[n++] = 'T'; + return scr_str_new(buf, n); +} + /* ── new Date(dateString).getTime() ────────────────────────────────────── * * The BOUNDED date-string parse (documented divergence — V8's parser diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 84db27075..029a75d05 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -101,6 +101,9 @@ double scr_date_get_time(double ms); * 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or * |ms| > 8.64e15 and returns NULL; +1 otherwise. */ ScrStr *scr_date_to_iso(double ms); +/* Node's Date.prototype.toUTCString ("Thu, 01 Jan 1970 00:00:00 GMT"; + * "Invalid Date" for NaN). Never throws; +1. */ +ScrStr *scr_date_to_utc_string(double ms); /* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity * shape + ECMA's format with an explicit offset); NaN elsewhere. */ double scr_date_parse_get_time(ScrStr *s); diff --git a/tests/corpus/date-to-utc-string.ts b/tests/corpus/date-to-utc-string.ts new file mode 100644 index 000000000..633de5439 --- /dev/null +++ b/tests/corpus/date-to-utc-string.ts @@ -0,0 +1,26 @@ +// Date.prototype.toUTCString: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a +// leading "-" and at least four digits for negative years, "Invalid Date" +// for an invalid time value. +const times = [ + 0, + -1, + 1445412480000, + 951782400000, // 2000-02-29 + Date.UTC(1969, 11, 31, 23, 59, 59, 999), + Date.UTC(0, 0, 1) - 1900 * 365.2425 * 86400000, + Date.UTC(-1, 5, 15, 12, 30, 45), + Date.UTC(-271821, 3, 20), + 8.64e15, + -8.64e15, + 1234.9, + -1234.9, + NaN, + 8.64e15 + 1, +]; +for (const t of times) { + const d = new Date(t); + console.log(d.toUTCString()); +} +for (let day = 0; day < 7; day++) console.log(new Date(Date.UTC(2024, 0, day + 1)).toUTCString()); +for (let month = 0; month < 12; month++) console.log(new Date(Date.UTC(2023, month, 9, 8, 7, 6)).toUTCString()); +console.log(new Date("2015-10-21T07:28:00Z").toUTCString(), new Date("garbage").toUTCString()); diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..caa3b4e72 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -552,6 +552,7 @@ describe("library profile fences", () => { "stdlib.date.now", "stdlib.date.parse", "stdlib.date.toISOString", + "stdlib.date.toUTCString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); From 6c1af69d038379f02ab4a33938729904003a7ee5 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:25:55 -0700 Subject: [PATCH 23/67] Add static Response.json `Response.json(data, init)` was refused (SC2020). It now builds a native Response the way undici does: ResponseInit converts, the data serializes through the checked-dynamic JSON serializer (toJSON hooks included; TypeError "Value is not JSON serializable" when JSON.stringify answers undefined), then the response initializes like the constructor with "application/json" as the default content type, so status, statusText, header, and null-body-status errors keep their order. The fetch compatibility profile moves Response.json from unsupported to a static operation with the static-response-json differential fixture. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/lib-shared.ts | 1 + packages/compiler/src/compat/fetch-profile.ts | 12 +++- .../src/frontend/lowering/lower-calls.ts | 3 + .../src/frontend/lowering/lower-stdlib.ts | 38 +++++++++++- packages/compiler/src/ir/builtin-effects.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 1 + packages/runtime/src/scr_fetch.c | 61 +++++++++++++++++-- packages/runtime/src/scr_generator.h | 1 + .../fetch/static-response-json/main.mts | 30 +++++++++ tests/harness/fetch.test.ts | 1 + 10 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/fetch/static-response-json/main.mts diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d5aebf0d8..82b339b05 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -810,6 +810,7 @@ export const LIB_FN_SYMS: Record = { // ambient-this read never throw. The fs dyn read is the sync-fs story. "fetch.start": "scr_fetch_static", "fetch.responseNew": "scr_fetch_response_new", + "fetch.responseJsonStatic": "scr_fetch_response_json_static", "fetch.input": "scr_fetch_input", "abort.controllerNew": "scr_fetch_abort_controller_new", "event.targetNew": "scr_event_target_new", diff --git a/packages/compiler/src/compat/fetch-profile.ts b/packages/compiler/src/compat/fetch-profile.ts index c1b43a810..c4d1e3f50 100644 --- a/packages/compiler/src/compat/fetch-profile.ts +++ b/packages/compiler/src/compat/fetch-profile.ts @@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = { evidence: [fixture("static")], }), ), + { + id: "stdlib.response.static.json", + name: "Response.json", + kind: "static-method", + facets: ["webidl-conversion", "error-shape"], + scope: + "data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor", + evidence: [fixture("static-response-json")], + }, ...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({ id: `stdlib.response.${member}`, name: `Response.${member}`, @@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = { ), staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"), - ...["error", "json", "redirect"].map((member) => + staticEntry("stdlib.response.static.json", "Response", "json", "static"), + ...["error", "redirect"].map((member) => unsupportedEntry( `stdlib.response.static.${member}`, "Response", diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index cd96f2aae..91ef32181 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,6 +1,7 @@ import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; import { lowerHashStreamCall } from "./builtins/hash-stream.js"; +import { lowerResponseStaticJson } from "./lower-stdlib.js"; import { lowerArrayCallbackCall } from "./containers/array-callback-call.js"; /** Dispatch source calls, builtin members and receiver-specific operations. * Signature collection, argument completion and function bodies have their @@ -1389,6 +1390,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // compile-time-known, so a literal key folds to a constant. lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ?? lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ?? + // Static Response.json(data, init?) builds a native Response. + lowerResponseStaticJson(lowerer, expr) ?? // Refuse Response constructor operations before generic-call // fallbacks can report an unrelated SC1090 diagnostic. lowerer.fenceUnsupportedFetchConstructorMember(expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 20caf184b..9fbe87c3d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -13,6 +13,7 @@ import { type IrExpr, type IrStmt, type IrType, + type SrcLoc, STRING, VOID, canConvertToDyn, @@ -1932,11 +1933,39 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; const args = expr.arguments ?? []; if (args.some(ts.isSpreadElement)) return null; - const loc = locOf(expr); + return lowerResponseConstruction(lowerer, args, locOf(expr), false); +} +/** Static `Response.json(data, init?)`: the data crosses into the runtime's + * checked-dynamic JSON serializer (toJSON hooks included) after ResponseInit + * converts, then the response initializes as the constructor does. */ +export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; + const access = call.expression; + if (access.questionDotToken || access.name.text !== "json") return null; + if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null; + if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null; + return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true); +} + +function lowerResponseConstruction( + lowerer: Lowerer, + args: readonly ts.Expression[], + loc: SrcLoc, + json: boolean, +): IrExpr { const toDyn = (node: ts.Expression | undefined, value: IrExpr, what: string): IrExpr => { if (node === undefined) return dynUndefinedExpr(loc); if (value.type.kind === "dyn") return value; + if (what === "JSON data") { + const boxed = lowerer.coerceToExpected(value, DYN); + if (boxed.type.kind === "dyn") return boxed; + lowerer.noLowering( + `Response.json of '${lowerer.fmt(value.type)}' values`, + node, + "the data must cross the checked-dynamic boundary (number, string, boolean, null, records, arrays, and 'unknown')", + ); + } /* A user class can satisfy ResponseInit structurally through plain * fields. Project just the declared dictionary members into a record, * then use the ordinary checked-dynamic copy. objRecordWidthHelper @@ -2059,8 +2088,11 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx } const result: IrExpr = { kind: "libCall", - fn: "fetch.responseNew", - args: [toDyn(bodyNode, bodyRef, "a body"), toDyn(initNode, initRef, "an init value")], + fn: json ? "fetch.responseJsonStatic" : "fetch.responseNew", + args: [ + toDyn(bodyNode, bodyRef, json ? "JSON data" : "a body"), + toDyn(initNode, initRef, "an init value"), + ], type: DYN, loc, }; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 1bb9cbd14..19082006b 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.requestNew", "fetch.headersNew", "fetch.responseNew", + "fetch.responseJsonStatic", "fetch.abortTimeout", "fetch.abortAny", "abort.timeout", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index a3462c7d8..952df7579 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -64,6 +64,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * are opaque checked-dynamic handles. */ "fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } }, "fetch.responseNew": { argTypes: [DYN, DYN], result: DYN }, + "fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN }, "fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } }, "abort.controllerNew": { argTypes: [], result: DYN }, /** EventTarget / Event / CustomEvent construction (scr_event_target.c): diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 7aba5a4b2..a3013c01b 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -4667,14 +4667,33 @@ static ScrDyn *sf_response_init_get( return NULL; } -ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { +/* The Response constructor, and Response.json(data, init) when json_data + * is non-NULL: undici converts ResponseInit, serializes the data (TypeError + * "Value is not JSON serializable" when JSON.stringify answers undefined), + * then initializes the response exactly as the constructor does, with + * "application/json" as the default content type. */ +typedef ScrDyn *(*SfJsonSerialize)(const ScrDyn *value); + +/* The JSON serializer reaches the constructor only through Response.json's + * entry point, so programs that never call it link no JSON walker. */ +static ScrDyn *sf_response_json_serialize(const ScrDyn *value) { + ScrStr *gap = scr_str_new("", 0); + ScrDyn *serialized = scr_json_stringify_replacer(value, scr_dyn_undefined(), gap); + scr_str_release(gap); + return serialized; +} + +static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init, + const ScrDyn *json_data, + SfJsonSerialize serialize) { ScrArr *header_pairs = NULL; ScrArr *converted_headers = NULL; ScrBytes *body_bytes = NULL; ScrStr *status_text = NULL; SfStream *stream = NULL; - bool null_body = - !body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL; + bool json_body = json_data != NULL; + bool null_body = !json_body && + (!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL); bool text_body = false; int status = 200; bool status_range_error = false; @@ -4683,7 +4702,7 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { * string arm therefore runs before ResponseInit's dictionary conversion, * while a ReadableStream is only brand-converted here — its locked/ * disturbed state belongs to the later body-extraction phase. */ - if (!null_body && + if (!null_body && !json_body && !(body->kind == SCR_DYN_HANDLE && body->v.handle.tag == SCR_DYNH_WEB_STREAM)) { if (body->kind == SCR_DYN_BYTES) { @@ -4757,7 +4776,25 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { /* Extract BodyInit before validating the response metadata. This makes a * locked/disturbed stream win over the later status range, statusText HTTP * reason-phrase, and HeadersInit validation exactly as in pinned Node. */ - if (null_body) { + if (json_body) { + ScrDyn *serialized = serialize(json_data); + if (!serialized) goto fail; + if (serialized->kind != SCR_DYN_STR) { + static const char unserializable[30] = "Value is not JSON serializable"; + scr_dyn_release(serialized); + scr_throw_error_msg(SCR_ERR_TYPE, unserializable, sizeof unserializable); + goto fail; + } + ScrStr *encoding = scr_str_new("utf8", 4); + body_bytes = scr_bytes_from_str(serialized->v.str, encoding); + scr_str_release(encoding); + scr_dyn_release(serialized); + stream = sf_stream_new_native(); + sf_stream_enqueue_bytes(stream, body_bytes); + sf_stream_close(stream); + scr_bytes_release(body_bytes); + body_bytes = NULL; + } else if (null_body) { stream = sf_stream_new_native(); sf_stream_close(stream); } else if (body->kind == SCR_DYN_HANDLE && @@ -4808,6 +4845,11 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { sf_push_header_text(header_pairs, "content-type", "text/plain;charset=UTF-8", 24); } + if (json_body && !sf_pairs_have(header_pairs, "content-type")) { + static const char json_type[16] = "application/json"; + scr_arr_push_ref(header_pairs, scr_str_new("content-type", 12)); + scr_arr_push_ref(header_pairs, scr_str_new(json_type, sizeof json_type)); + } SfResponse *response = calloc(1, sizeof *response); if (!response) sf_oom(); @@ -4833,6 +4875,15 @@ fail: return NULL; } +ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) { + return sf_response_construct(body, init, NULL, NULL); +} + +ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init) { + return sf_response_construct(NULL, init, data ? data : scr_dyn_undefined(), + &sf_response_json_serialize); +} + /* Request uses the same body stream and Headers ownership as Response. */ typedef struct SfRequest { size_t rc; diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 2c2c0e953..605cabec5 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -163,6 +163,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init); ScrDyn *scr_fetch_headers_new(ScrDyn *init); ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */ +ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */ ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ diff --git a/tests/fixtures/fetch/static-response-json/main.mts b/tests/fixtures/fetch/static-response-json/main.mts new file mode 100644 index 000000000..3bbde4977 --- /dev/null +++ b/tests/fixtures/fetch/static-response-json/main.mts @@ -0,0 +1,30 @@ +// Response.json(data, init): ResponseInit converts, the data serializes with +// JSON.stringify (TypeError when it answers undefined), then the response +// initializes like the constructor, defaulting content-type to +// application/json. +function show(label: string, make: () => Response): void { + try { + const r = make(); + console.log(label, r.status, JSON.stringify(r.statusText), r.headers.get("content-type"), r.headers.get("x-a")); + } catch (e) { + console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : String(e)); + } +} +show("plain", () => Response.json({ ok: true })); +show("init", () => Response.json([1, 2], { status: 201, statusText: "Made", headers: { "x-a": "1" } })); +show("content-type", () => Response.json("s", { headers: { "content-type": "text/x" } })); +show("headers object", () => Response.json(null, { headers: new Headers([["x-a", "h"]]) })); +show("undefined", () => Response.json(undefined)); +const fn: unknown = () => 1; +show("function", () => Response.json(fn)); +show("range", () => Response.json({}, { status: 99 })); +show("undefined before range", () => Response.json(undefined, { status: 99 })); +show("null body status", () => Response.json({}, { status: 204 })); +show("bad header", () => Response.json({}, { headers: { "bad name": "x" } })); +show("undefined before bad header", () => Response.json(undefined, { headers: { "bad name": "x" } })); +show("bad statusText", () => Response.json({}, { statusText: "a\nb" })); +const data: unknown = JSON.parse('{"a":[1,"x",null],"n":{"deep":true}}'); +const r = Response.json({ data, d: new Date(0), skip: undefined }); +console.log(r.bodyUsed, await r.text(), r.bodyUsed); +const parsed: unknown = await Response.json({ round: [1, 2] }).json(); +console.log(JSON.stringify(parsed)); diff --git a/tests/harness/fetch.test.ts b/tests/harness/fetch.test.ts index 6b04acf59..337eafec6 100644 --- a/tests/harness/fetch.test.ts +++ b/tests/harness/fetch.test.ts @@ -163,6 +163,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => { "static-listener-noncallable", "static-abort-throw", "static-dispatch-throw", + "static-response-json", ] as const; test.for( staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)), From 30e23af63ff1b03bcfc50bca4ce4c942fa9433da Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:27:35 -0700 Subject: [PATCH 24/67] Store fetch Headers, Request, and Response handles in class fields A class field typed `Headers`, `Request`, or `Response` (or an array or nullable form of them) maps to checked-dynamic storage, and the class-field rule refused it (SC1090) although `unknown`, Map values, and closures already store the same native handles. Such fields now take the checked-dynamic slot, like the worker handles; reads keep the handle's identity and the static fetch member lowering. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 7 ++-- packages/compiler/src/frontend/type-mapper.ts | 34 ++++++++++++++++++ tests/corpus/class-fetch-handle-fields.ts | 36 +++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/corpus/class-fetch-handle-fields.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 6a24b5417..a10874928 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2680,7 +2680,7 @@ export function collectClassShapeInner( type.kind === "void" || (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile()) && - !lowerer.classImplementedProtocol(inferred)) + !lowerer.classFieldStoresDyn(inferred)) ) lowerer.badType(member, inferred); if (isJsSourceFile(member.getSourceFile())) { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 145fb3ae6..f933b8f5c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -235,6 +235,7 @@ import { isOsConstantsSymbol, isUnitOnlyTsType, isWorkerHandleType, + isFetchHandleType, jsOpenObjectType, mapType, ShapeRegistry, @@ -3004,12 +3005,14 @@ export class Lowerer { /** Declared class fields may store a DYN value only when its static type * is `unknown` or `object` (also with `null` or `undefined`, or an array * of those), names checked protocol identity, or is an owned runtime - * handle (a Worker or MessagePort); `any` fields stay refused. */ + * handle (a Worker or MessagePort, or a fetch Headers/Request/Response); + * `any` fields stay refused. */ classFieldStoresDyn(type: ts.Type): boolean { return ( this.opaqueValueType(type) || this.classImplementedProtocol(type) || - isWorkerHandleType(type, this.typeCtx) + isWorkerHandleType(type, this.typeCtx) || + isFetchHandleType(type, this.typeCtx) ); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index f37703fd8..8089304f0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1313,6 +1313,40 @@ export function isWorkerHandleType( return handle; } +/** The native fetch handles (Headers, Request, Response), optionally in + * arrays or with null/undefined: checked-dynamic handles wherever they are + * stored, like the worker handles above. */ +export function isFetchHandleType( + type: ts.Type, + ctx: Pick, +): boolean { + const members = type.isUnionType() ? ts.constituentTypes(type) : [type]; + let handle = false; + for (const member of members) { + if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue; + if (ctx.checker.isArrayType(member)) { + const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0]; + if (element === undefined || !isFetchHandleType(element, ctx)) return false; + } else { + const symbol = member.getSymbol(); + if ( + !symbol || + (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || + !ctx.checker + .declarationsOf(symbol) + .some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) + return false; + } + handle = true; + } + return handle; +} + export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null; // These intrinsic domains cannot depend on generic bindings, declaration diff --git a/tests/corpus/class-fetch-handle-fields.ts b/tests/corpus/class-fetch-handle-fields.ts new file mode 100644 index 000000000..b06df1313 --- /dev/null +++ b/tests/corpus/class-fetch-handle-fields.ts @@ -0,0 +1,36 @@ +// Class fields typed as native fetch handles (Headers, Request, Response), +// directly, optional, nullable, in arrays, and inside record fields, hold +// the live handle: mutation through one reference is seen through another. +class Holder { + headers: Headers; + list: Headers[] = []; + maybe?: Response; + req: Request | null = null; + bag: { headers: Headers; status: number }; + constructor(headers: Headers) { + this.headers = headers; + this.bag = { headers, status: 1 }; + } +} +const holder = new Holder(new Headers({ a: "1" })); +console.log(holder.headers.get("a"), holder.list.length, holder.maybe === undefined, holder.req); +holder.list.push(new Headers({ b: "2" })); +holder.maybe = new Response("x", { status: 201 }); +holder.req = new Request("http://localhost/p", { method: "POST" }); +console.log(holder.list[0]!.get("b"), holder.maybe.status, holder.req.method, holder.req.url); +holder.headers.set("c", "3"); +console.log(holder.bag.headers.get("c"), holder.bag.headers === holder.headers); + +// Symbol-keyed fields hold the handles the same way. +const NATIVE = Symbol("native"); +class Wrapper { + [NATIVE]: Response; + constructor(body: string) { + this[NATIVE] = new Response(body, { status: 202 }); + } + get status(): number { + return this[NATIVE].status; + } +} +const wrapped = new Wrapper("ok"); +console.log(wrapped.status, wrapped[NATIVE].headers.get("content-type")); From e6dbad9567c88078c08b25b1ff9dd667866a5f76 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:27:35 -0700 Subject: [PATCH 25/67] Destructure Maps and Sets with array patterns `const [[k, v], ...rest] = map` was refused (SC1031). The source now snapshots once into its iteration order (Map entries as pairs, Set values), the spread's own machinery, and the pattern lowers as an array pattern over that snapshot, as string sources already do. Element defaults keep a fence because they run between iterator steps. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-stmts.ts | 34 +++++++++++++++++++ tests/corpus/destructure-map-set-iterables.ts | 18 ++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/corpus/destructure-map-set-iterables.ts diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 12544979c..d6247a9ef 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,4 +1,5 @@ import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; +import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { importMetaBindingSource } from "./import-meta.js"; @@ -2380,6 +2381,39 @@ export function lowerBindingPattern( }); return; } + // Map and Set sources (`const [[k, v], ...rest] = map`): the iterator + // yields the collection's entries (Map pairs) or values in order, so + // the source snapshots ONCE into that array — the spread's own + // machinery — and the pattern lowers as an array pattern over it, + // like the string case above. Element defaults keep a fence: they + // run between iterator steps and could mutate the live collection. + if (srcType.kind === "map" || srcType.kind === "set") { + const defaulted = pattern.elements.find( + (el): el is ts.BindingElement => + ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken, + ); + if (defaulted) + lowerer.unsupported("SC1031", defaulted, "binding defaults over Map/Set sources"); + const snapshot = lowerCollectionSpread( + lowerer, + srcRef(), + pattern as unknown as ts.Expression, + ); + if (snapshot && snapshot.type.kind === "array") { + const loc = locOf(pattern); + const snapshotT = snapshot.type; + const entries = lowerer.declareHiddenLocal("%dentries", snapshotT); + out.push({ kind: "varDecl", localId: entries.id, init: snapshot, loc }); + lowerer.lowerBindingPattern( + pattern, + () => ({ kind: "varRef", localId: entries.id, type: snapshotT, loc }), + snapshotT, + isLet, + out, + ); + return; + } + } if (srcType.kind !== "array") { lowerer.unsupported( "SC1031", diff --git a/tests/corpus/destructure-map-set-iterables.ts b/tests/corpus/destructure-map-set-iterables.ts new file mode 100644 index 000000000..295a224d8 --- /dev/null +++ b/tests/corpus/destructure-map-set-iterables.ts @@ -0,0 +1,18 @@ +// Array destructuring of a Map or Set walks its iterator: Map entries as +// [key, value] pairs, Set values, holes and rest included. +const m = new Map([["a", "1"], ["b", "2"], ["c", "3"]]); +const [[name, value], ...rest] = m; +console.log(name, value, rest.length, JSON.stringify(rest)); +const [first, , third] = m; +console.log(JSON.stringify(first), JSON.stringify(third)); +const s = new Set([3, 1, 2]); +const [x, y, ...others] = s; +console.log(x, y, others); +let [p] = new Map([["only", 1]]); +p = ["changed", 2]; +console.log(p); +function firstKey(map: Map): string { + const [[k]] = map; + return k; +} +console.log(firstKey(new Map([["z", 26]]))); From a8b7bafefb6b95b8c27eb2dd347ee3b794bd101f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:29:57 -0700 Subject: [PATCH 26/67] Lower `...(cond && { k: v })` spreads as conditional fields Spreading a falsy primitive copies nothing, so `...(cond && { k: v })` is the optional-field idiom `...(cond ? { k: v } : {})`. The `&&` spelling (the @edge-runtime/cookies parseSetCookie shape) was refused with a record shape mismatch (SC2002). When the spread's type mixes the record with primitive arms, the literal lowering now uses the existing conditional-spread desugar: cond evaluates once, v only when taken, and the absent arm leaves the optional field undefined. Record and record-or-unit spreads keep the source-copy path. Co-Authored-By: Claude Opus 5.5 --- .../lowering/expressions/object-literals.ts | 68 +++++++++++++++++-- tests/corpus/object-spread-and-conditional.ts | 37 ++++++++++ 2 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 tests/corpus/object-spread-and-conditional.ts diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index c95731fc7..78d783a11 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -823,7 +823,7 @@ export function lowerObjectLiteral( // `k?: ...` — which desugar to one conditional field at the // spread's own position (any position: they introduce one fresh // name, collision-fenced below). Everything else stays fenced. - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { lowerer.unsupported( "SC1090", @@ -1264,6 +1264,7 @@ export function lowerObjectLiteral( expr.properties.length >= 2 && ts.isSpreadAssignment(expr.properties[0]!) && !conditionalSpreadOf(expr.properties[0]!.expression) && + !andSpreadOf(lowerer, expr.properties[0]!.expression) && expr.properties .slice(1) .every( @@ -1340,7 +1341,7 @@ export function lowerObjectLiteral( let mergeable = true; for (const prop of expr.properties) { if (ts.isSpreadAssignment(prop)) { - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { lowerer.unsupported( "SC1090", @@ -1481,7 +1482,12 @@ export function lowerObjectLiteral( !shape.tuple && shape.fields.length > 0 && expr.properties.length > 0 && - expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) && + expr.properties.every( + (p) => + ts.isSpreadAssignment(p) && + !conditionalSpreadOf(p.expression) && + !andSpreadOf(lowerer, p.expression), + ) && expr.properties.some((p) => { const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression)); return ( @@ -1599,7 +1605,7 @@ export function lowerObjectLiteral( } } if (ts.isSpreadAssignment(prop)) { - const cs = conditionalSpreadOf(prop.expression); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs && cs !== "unsupported") { // `...(c ? { k: v } : {})` — ONE conditional field at the // spread's own position: cond evaluates exactly once, `v` only @@ -1728,7 +1734,8 @@ export function lowerObjectLiteral( const laterNames = new Set(); for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { - if (conditionalSpreadOf(later.expression)) continue; + if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) + continue; const lt = laterSpreadType(lowerer, later.expression); if (lt?.kind === "record") { // A field the later source may lack defines nothing. @@ -1963,7 +1970,8 @@ export function lowerObjectLiteral( const laterNames = new Set(); for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { - if (conditionalSpreadOf(later.expression)) continue; + if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) + continue; const lt = laterSpreadType(lowerer, later.expression); if (lt?.kind === "record") { // A field the later source may lack defines nothing. @@ -2517,6 +2525,54 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio }; } +/** `...(c && { k: v })`: spreading a falsy primitive (false, 0, "", NaN) + * copies nothing, so the idiom is `...(c ? { k: v } : {})` with `c` + * evaluated once. Recognized only where the spread's own type mixes the + * record with primitive arms; a record or record-or-unit spread keeps the + * ordinary source-copy path. */ +export function andSpreadOf( + lowerer: Lowerer, + expr: ts.Expression, +): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" | null { + let e: ts.Expression = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) + return null; + let right: ts.Expression = e.right; + while (ts.isParenthesizedExpression(right)) right = right.expression; + if (!ts.isObjectLiteralExpression(right)) return null; + const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e)); + if (spreadType?.kind !== "union") return null; + const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? []; + if ( + arms.filter((arm) => arm.kind === "record").length !== 1 || + !arms.some((arm) => arm.kind === "bool" || arm.kind === "f64" || arm.kind === "string") || + !arms.every( + (arm) => + arm.kind === "record" || + arm.kind === "bool" || + arm.kind === "f64" || + arm.kind === "string" || + isUnitType(arm), + ) + ) + return null; + if (right.properties.length === 0) return "unsupported"; + const props: CondSpreadProp[] = []; + for (const p of right.properties) { + if ( + (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && + p.name && + (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) + ) { + props.push(p as CondSpreadProp); + } else { + return "unsupported"; + } + } + return { cond: e.left, props, whenTrue: true }; +} + /** A conditional spread's carrier property: `name: value` or shorthand, * with an identifier/string-literal name. */ export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & { diff --git a/tests/corpus/object-spread-and-conditional.ts b/tests/corpus/object-spread-and-conditional.ts new file mode 100644 index 000000000..dd42063d0 --- /dev/null +++ b/tests/corpus/object-spread-and-conditional.ts @@ -0,0 +1,37 @@ +// `...(cond && { k: v })` spreads nothing when cond is a falsy primitive +// (false, 0, ""), so it is the optional-field idiom `...(cond ? { k: v } : {})`: +// cond evaluates once, v only when taken. (Fields print one by one: the +// declared-shape property order of spread-built records is a documented +// limitation.) +type C = { name: string; name2: string; httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: string }; +function parse(attrs: Record, flag: boolean): C { + const httponly = attrs.httponly; + const maxage = attrs["max-age"]; + const samesite = attrs.samesite; + let calls = 0; + const lower = (s: string): string => { + calls++; + return s.toLowerCase(); + }; + const c: C = { + name: "a", + ...(flag && { httpOnly: true }), + ...(httponly && { path: httponly }), + ...(typeof maxage === "string" && { maxAge: Number(maxage) }), + name2: "/", + ...(samesite && { sameSite: lower(samesite) }), + }; + console.log("calls", calls); + return c; +} +for (const [attrs, flag] of [ + [{}, false], + [{ httponly: "yes", "max-age": "10", samesite: "Lax" }, true], + [{ httponly: "", "max-age": "", samesite: "" }, true], +] as [Record, boolean][]) { + const c = parse(attrs, flag); + console.log(c.name, c.name2, c.httpOnly, c.path, c.maxAge, c.sameSite, "httpOnly" in c, "path" in c); +} +const n: number = Number(process.argv.length > 99 ? "1" : "0"); +const d: { v?: number; name: string } = { name: "n", ...(n && { v: n }) }; +console.log(d.name, d.v, "v" in d); From dd0c255fa7a7b56550ab1a09df974e97aba92424 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 27/67] Stringify class instances that declare toJSON `JSON.stringify(new Url(...))` was refused (SC1090) for every program class, although JSON.stringify of a class with a toJSON hook serializes whatever toJSON(key) returns. A class whose chain declares toJSON now crosses into the runtime JSON walker through its checked-dynamic class view, which calls the hook with the root key "" and handles nested hooks; an undefined hook result keeps the dyn-root undefined rule. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/builtins/json.ts | 11 ++++- tests/corpus/json-stringify-class-tojson.ts | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/json-stringify-class-tojson.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7ce902772..8cce83beb 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -149,9 +149,18 @@ export function lowerJsonMethodCall( } const optionalString = lowerOptionalStringifyRoot(lowerer, value, indent, loc); if (optionalString) return optionalString; + // A program class instance whose class (or a base) declares toJSON: + // JSON.stringify serializes whatever toJSON(key) returns, so the + // instance crosses into the runtime walker, which calls the hook + // through the checked-dynamic class view. + const toJsonClass = + value.type.kind === "object" && + lowerer.findMethodOn(lowerer.classes.get(value.type.className) ?? null, "toJSON") !== null && + lowerer.dynConvertible(value.type); if ( value.type.kind === "url" || value.type.kind === "dyn" || + toJsonClass || isJsonStringifyDynamicType( value.type, (id) => lowerer.shapes.get(id), @@ -182,7 +191,7 @@ export function lowerJsonMethodCall( // symbol). Where the program can observe that (jsonResultObserved), // keep the actual undefined; elsewhere the dyn-root string ABI // stays, whose undefined result prints the same text. - return value.type.kind === "dyn" && !jsonResultObserved(lowerer, call) + return (value.type.kind === "dyn" || toJsonClass) && !jsonResultObserved(lowerer, call) ? { kind: "toString", operand: raw, type: STRING, loc } : { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc }; } diff --git a/tests/corpus/json-stringify-class-tojson.ts b/tests/corpus/json-stringify-class-tojson.ts new file mode 100644 index 000000000..617e5ac25 --- /dev/null +++ b/tests/corpus/json-stringify-class-tojson.ts @@ -0,0 +1,44 @@ +// JSON.stringify of a class instance whose class (or a base) declares +// toJSON serializes what toJSON(key) returns: the root key is "", nested +// hooks run through the same walker, and a toJSON answering undefined +// makes the whole result undefined. +class Url { + href: string; + constructor(href: string) { + this.href = href; + } + toJSON(): string { + return this.href; + } +} +class NextUrl extends Url { + base = "ignored"; +} +class Keyed { + toJSON(key: string): { key: string; at: Url[] } { + return { key, at: [new Url("n")] }; + } +} +class Missing { + toJSON(): undefined { + return undefined; + } +} +console.log(JSON.stringify(new Url("http://x/"))); +console.log(JSON.stringify(new NextUrl("http://y/"))); +console.log(JSON.stringify(new Keyed())); +console.log(JSON.stringify(new Keyed(), null, 2)); +function serialize(value: Missing): string { + const text = JSON.stringify(value); + return text === undefined ? "undefined result" : `text ${text}`; +} +console.log(serialize(new Missing()), JSON.stringify(new Missing()) === undefined); +let calls = 0; +class Counted { + toJSON(): number { + calls++; + return calls; + } +} +const c = new Counted(); +console.log(JSON.stringify(c), JSON.stringify(c), calls); From cd63b935e0dd280eaef5e1fd9c4488ba8d07ffe0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 28/67] Instantiate generics with a literal undefined argument `id(undefined)` binds T = undefined, which the type mapper answers as VOID (the return convention), so the instance's parameter was refused (SC2001) although a declared `v: undefined` parameter compiles. Instantiated parameters are value positions and now take the unit-only slot, as declared ones do. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/call-signatures.ts | 14 ++++++++++++-- tests/corpus/generic-undefined-argument.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/corpus/generic-undefined-argument.ts diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 8e30e8b82..65a013bf3 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -13,7 +13,12 @@ import { typeEquals, } from "../../ir/ir.js"; import { isJsSourceFile, isNodeEsmFile } from "../program.js"; -import { genResultRecord, jsOpenObjectType } from "../type-mapper.js"; +import { + genResultRecord, + isUnitOnlyTsType, + jsOpenObjectType, + unitOnlyUnion, +} from "../type-mapper.js"; import { dynFallbackType } from "./lowerer.js"; import { builtinModuleFnOf } from "./surfaces.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; @@ -373,7 +378,7 @@ export function paramShapes( const tsType = symbol ? lowerer.checker.getTypeOfSymbol(symbol) : lowerer.typeOf(declParam.name); - const mapped = jsOpenObjectType( + let mapped = jsOpenObjectType( declParam, lowerer.runtimeOptionalBindingType( declParam.name, @@ -382,6 +387,11 @@ export function paramShapes( lowerer.shapes, lowerer.unions, ); + // An instantiation with a literal `undefined` argument (`id(undefined)` + // binds T = undefined): standalone undefined maps to VOID (the return + // convention), and a parameter is a value position, so it takes the + // unit-only slot like a declared `v: undefined` parameter. + if (mapped?.kind === "void" && isUnitOnlyTsType(tsType)) mapped = unitOnlyUnion(lowerer.unions); if (!mapped || mapped.kind === "void") lowerer.badType(blameOf?.(declParam, i) ?? declParam.name, tsType); if (declParam.dotDotDotToken) { diff --git a/tests/corpus/generic-undefined-argument.ts b/tests/corpus/generic-undefined-argument.ts new file mode 100644 index 000000000..97d438549 --- /dev/null +++ b/tests/corpus/generic-undefined-argument.ts @@ -0,0 +1,19 @@ +// A generic call with a literal `undefined` argument binds T = undefined; +// the parameter holds undefined like a declared `v: undefined` parameter. +function kind(v: T): string { + return typeof v; +} +console.log(kind(undefined), kind(null), kind(1), kind("s")); +function same(a: T, b: T): boolean { + return a === b; +} +console.log(same(undefined, undefined)); +function wrap(v: T): { v: T } { + return { v }; +} +const w = wrap(undefined); +console.log(JSON.stringify(w), w.v === undefined, "v" in w); +function describe(label: string, v: T): string { + return `${label}=${String(v)}`; +} +console.log(describe("u", undefined)); From b831d840d73f32787ed67388ec1aa72701af1d95 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:34:42 -0700 Subject: [PATCH 29/67] Accept getters that never return a value A getter that only throws (`get page(): never`, or Next.js's unannotated removed-API getters) or has no return statement declared a VOID property type, so the class was refused (SC2001). No read of such a getter produces a value other than undefined; the property now takes the checked-dynamic slot, and each read throws the getter's error or answers undefined. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 12 ++++++- tests/corpus/class-never-getter.ts | 36 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/class-never-getter.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a10874928..f68ff45da 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3465,7 +3465,17 @@ export function collectClassShapeInner( } let sig: { params: ParamShape[]; ret: IrType }; if (isGet) { - const ret = lowerer.declaredReturnType(member, member.name); + let ret = lowerer.declaredReturnType(member, member.name); + // A getter that never returns a value: typed `never` (it only + // throws, Next.js's removed `page`/`ua` getters) or, unannotated + // with no return statement, inferred `void`. A read answers + // undefined or throws, so the property takes the checked-dynamic + // slot instead of VOID. + if ( + ret.kind === "void" && + (lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0 + ) + ret = DYN; if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); sig = { params: [], ret }; } else { diff --git a/tests/corpus/class-never-getter.ts b/tests/corpus/class-never-getter.ts new file mode 100644 index 000000000..497fa3d25 --- /dev/null +++ b/tests/corpus/class-never-getter.ts @@ -0,0 +1,36 @@ +// A getter that never returns a value compiles: annotated `never` or +// unannotated (the removed-API getter idiom, which throws), and a getter +// without a return statement, whose reads answer undefined. +class Req { + ok = 1; + get page(): never { + throw new Error("page has been removed"); + } + get ua(): never { + throw new TypeError("ua has been removed"); + } + public get legacy() { + throw new RangeError("legacy has been removed"); + } + get nothing() { + return; + } +} +const r = new Req(); +try { + console.log(r.page); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +try { + const ua = r.ua; + console.log("unreachable", ua); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +try { + console.log(r.legacy); +} catch (e) { + console.log(e instanceof Error ? `${e.name}: ${e.message}` : e); +} +console.log(r.ok, r.nothing); From 3149e7be9177dfd48e03f3c12ece5bee7e6f6970 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:36:51 -0700 Subject: [PATCH 30/67] Regenerate the surface manifest Date.prototype.toUTCString and the static Response.json operation are static surfaces now (pnpm manifest). pnpm node-compat leaves the Node ledger and the public artifact unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/surface-manifest.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index f5313f12d..a71cccaf4 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4159,6 +4159,13 @@ "status": "static", "note": "UTC ISO formatting over constructed and stored Date values" }, + { + "id": "stdlib.date.toUTCString", + "kind": "stdlib", + "name": "Date.prototype.toUTCString", + "status": "static", + "note": "RFC 7231 IMF-fixdate formatting over stored Date values" + }, { "id": "stdlib.date.valueOf", "kind": "stdlib", @@ -5236,9 +5243,8 @@ "id": "stdlib.response.static.json", "kind": "stdlib", "name": "Response.json", - "status": "unsupported", - "code": "SC2020", - "note": "Node 24.22.0 / Undici 7.29.1; Response static constructor-object operations have no compiler lowering" + "status": "static", + "note": "Node 24.22.0 / Undici 7.29.1; facets: webidl-conversion, error-shape; supported scope: data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor; differential evidence: fixture:static-response-json" }, { "id": "stdlib.response.static.redirect", From d84e410981499a181df64771c94d55c4dba66ef6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:46:52 -0700 Subject: [PATCH 31/67] Upcast devirtualized calls whose override returns a subclass A base method calling `this.set()` where only a subclass is constructed devirtualizes to the subclass override. With a polymorphic `this` return type, the override's lowered return type is the subclass object while the site keeps the base method's type, and the validator reported an internal compiler error (SC9001: "call %Sub.set type object != return object"). The Next.js extract hit this once its placeholder ResponseCookies instance was gone: MiddlewareResponseCookies became the only constructed class. The direct call now carries the callee's call-site return type and upcasts to the site's class; any other mismatch keeps the virtual call. Calls whose types already matched are emitted exactly as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/devirtualize.ts | 34 +++++++++++++++---- packages/compiler/src/ir/validate.ts | 2 +- .../corpus/class-devirtualized-this-return.ts | 27 +++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 tests/corpus/class-devirtualized-this-return.ts diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index 48a5712e3..04a53aeb1 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -1,6 +1,7 @@ import * as ts from "../ts7/adapter.js"; -import type { IrExpr, IrFunction } from "../../ir/ir.js"; +import { type IrExpr, type IrFunction, type IrType, typeEquals } from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; +import { callSiteReturnType } from "../../ir/validate.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; @@ -56,6 +57,9 @@ export function devirtualizeVirtualCalls( class ConstructedClasses { private readonly lowered = new Set(); + /** Lowered bodies' return types: an override may return a subclass + * (`set(): this` resolves per declaring class). */ + private readonly returnTypes = new Map(); /** Classes some retained IR allocates or exposes as a class value. */ private readonly allocated = new Set(); private readonly exportedNames: ReadonlySet; @@ -74,6 +78,7 @@ class ConstructedClasses { }; for (const fn of functions) { this.lowered.add(fn.name); + this.returnTypes.set(fn.name, callSiteReturnType(fn)); everyStmtList(fn.body, { stmt: () => true, expr: (e) => { @@ -102,7 +107,7 @@ class ConstructedClasses { this.facts.reachable.has(`%${candidate.def.name}.${e.method}`), ) ) - return this.direct(e, info, nearest.declarer); + return this.direct(e, info, nearest.declarer) ?? e; if (this.open(info)) return e; const targets: ClassInfo[] = []; for (const candidate of this.descendants(info)) { @@ -117,7 +122,7 @@ class ConstructedClasses { const callee = `%${target.def.name}.${e.method}`; if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e; } - if (targets.length === 1) return this.direct(e, info, targets[0]!); + if (targets.length === 1) return this.direct(e, info, targets[0]!) ?? e; // A small closed set: the backend tests the loaded slot against each // implementation and calls it directly (no indirect fallback; every // instance that can reach this site resolves to one of them). @@ -125,7 +130,19 @@ class ConstructedClasses { return { ...e, targets: targets.map((t) => t.def.name) }; } - private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr { + /** The direct call, or null when the target's return type cannot stand + * for the site's: an override returning a subclass object (a polymorphic + * `this` return) upcasts to the site's class; any other mismatch keeps + * the virtual call. */ + private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr | null { + const callee = `%${target.def.name}.${e.method}`; + const ret = this.returnTypes.get(callee) ?? e.type; + const upcast = + !typeEquals(ret, e.type) && + ret.kind === "object" && + e.type.kind === "object" && + this.lowerer.isSubclassOf(ret.className, e.type.className); + if (!typeEquals(ret, e.type) && !upcast) return null; const receiver = e.args[0]!; const typed: IrExpr = target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name) @@ -139,13 +156,16 @@ class ConstructedClasses { type: { kind: "object", className: target.def.name }, loc: receiver.loc, }; - return { + const call: IrExpr = { kind: "call", - callee: `%${target.def.name}.${e.method}`, + callee, args: [typed, ...e.args.slice(1)], - type: e.type, + type: ret, loc: e.loc, }; + return upcast && e.type.kind === "object" + ? this.lowerer.upcastTo(call, e.type.className) + : call; } /** The class and every class below it, in the isSubclassOf sense diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 09ab7d951..979b70115 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -90,7 +90,7 @@ export interface IrValidationError { * calls and closures over it see Promise; a generator body's is the * TReturn channel, so call sites see the generator type. The one place * the body/call-site split is spelled out in the validator. */ -function callSiteReturnType(fn: IrFunction): IrType { +export function callSiteReturnType(fn: IrFunction): IrType { if (fn.async && fn.generator !== undefined) { return { kind: "generator", diff --git a/tests/corpus/class-devirtualized-this-return.ts b/tests/corpus/class-devirtualized-this-return.ts new file mode 100644 index 000000000..e7f2fc9fa --- /dev/null +++ b/tests/corpus/class-devirtualized-this-return.ts @@ -0,0 +1,27 @@ +// A base method calling `this.set()` where only a subclass is ever +// constructed devirtualizes to the subclass override; a polymorphic `this` +// return resolves per declaring class, so the direct call's subclass +// result upcasts to the base method's type (was an internal compiler error). +class Cookies { + values: string[] = []; + set(value: string): this { + this.values.push(value); + return this; + } + delete(name: string): this { + return this.set(`${name}=; Max-Age=0`); + } +} +class MirroredCookies extends Cookies { + mirror: string[] = []; + override set(value: string): this { + super.set(value); + this.mirror.push(value.toUpperCase()); + return this; + } +} +const cookies = new MirroredCookies(); +cookies.set("a=1").delete("b").set("c=3"); +console.log(cookies.values.join(" | ")); +console.log(cookies.mirror.join(" | ")); +console.log(cookies.delete("d") === cookies, cookies.values.length); From 8e457b2b05a632bb919cd7ef208686ae3774b429 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 14:53:15 -0700 Subject: [PATCH 32/67] Record ts7 preflight/order baselines for the new corpus programs pnpm test:ts7 --baselines-only reported the fifteen new corpus programs as missing. Recorded with SCRIPTC_UPDATE_BASELINES=1 from the native frontend; every entry has no preflight diagnostics and a single-module order, and no existing baseline changed. Co-Authored-By: Claude Opus 5.5 --- .../tests/corpus/class-devirtualized-this-return.ts.json | 6 ++++++ .../tests/corpus/class-fetch-handle-fields.ts.json | 6 ++++++ .../order-parity/tests/corpus/class-never-getter.ts.json | 6 ++++++ .../corpus/class-symbol-field-constructor-assigned.ts.json | 6 ++++++ .../order-parity/tests/corpus/date-to-utc-string.ts.json | 6 ++++++ .../tests/corpus/destructure-map-set-iterables.ts.json | 6 ++++++ .../tests/corpus/fetch-headers-set-cookie-iteration.ts.json | 6 ++++++ .../tests/corpus/fetch-invalid-url-cause.ts.json | 6 ++++++ .../tests/corpus/fetch-request-set-cookie-wire.ts.json | 6 ++++++ .../tests/corpus/generic-undefined-argument.ts.json | 6 ++++++ .../tests/corpus/json-stringify-class-tojson.ts.json | 6 ++++++ .../corpus/json-stringify-unknown-undefined-root.ts.json | 6 ++++++ .../tests/corpus/object-spread-and-conditional.ts.json | 6 ++++++ .../tests/corpus/optional-call-native-method.ts.json | 6 ++++++ .../tests/corpus/union-narrowed-tostring.ts.json | 6 ++++++ 15 files changed, 90 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json new file mode 100644 index 000000000..f75299bf2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-devirtualized-this-return.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-devirtualized-this-return.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json new file mode 100644 index 000000000..69dfd8454 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-fetch-handle-fields.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-fetch-handle-fields.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json new file mode 100644 index 000000000..4b5ee103f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-never-getter.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-never-getter.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json new file mode 100644 index 000000000..4078df7c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-symbol-field-constructor-assigned.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-symbol-field-constructor-assigned.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json new file mode 100644 index 000000000..6c5e37d54 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/date-to-utc-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/date-to-utc-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json new file mode 100644 index 000000000..fee4d0ebc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/destructure-map-set-iterables.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/destructure-map-set-iterables.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json new file mode 100644 index 000000000..f0d413603 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-headers-set-cookie-iteration.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-headers-set-cookie-iteration.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json new file mode 100644 index 000000000..70f7a83fd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-invalid-url-cause.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-invalid-url-cause.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json new file mode 100644 index 000000000..9357bc1d8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fetch-request-set-cookie-wire.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/fetch-request-set-cookie-wire.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json new file mode 100644 index 000000000..bbc59d2ca --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-undefined-argument.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-undefined-argument.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json new file mode 100644 index 000000000..9b1aa55ac --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-class-tojson.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-class-tojson.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json new file mode 100644 index 000000000..8c424c6a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-stringify-unknown-undefined-root.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-stringify-unknown-undefined-root.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json new file mode 100644 index 000000000..c58247184 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/object-spread-and-conditional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/object-spread-and-conditional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json new file mode 100644 index 000000000..a8678d806 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/optional-call-native-method.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/optional-call-native-method.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json new file mode 100644 index 000000000..454df784e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-narrowed-tostring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-narrowed-tostring.ts" + ], + "diags": [] +} From 52303baf4b01f7f0fb3946ca36a178782776a833 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:01:49 -0500 Subject: [PATCH 33/67] Keep the promise of async generic functions whose body reads a missing element The instance result of an async generic function whose body returns an element or index-signature read that can miss is Promise: the promise is always present and settles with undefined. The call site widened the whole promise instead, so the instance and its calls had different signatures and every call failed to compile. --- .../src/frontend/lowering/generic-functions.ts | 7 +++++-- tests/corpus/5954-generic-optional-returns.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 90f83ec2c..1502f7fc5 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -298,9 +298,12 @@ export function genericCallInstance( lowerer.badType(expr, retTs); } // A body that can return an indexed read that misses returns undefined - // there, as in JavaScript, whatever the declared result says. + // there, as in JavaScript, whatever the declared result says. An async + // body's promise is always present and settles with undefined there. const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) - ? lowerer.runtimeOptionalType(mappedReturn) + ? mappedReturn.kind === "promise" + ? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) } + : lowerer.runtimeOptionalType(mappedReturn) : mappedReturn; // keyof-constrained type parameters (`K extends keyof T`): the bound diff --git a/tests/corpus/5954-generic-optional-returns.ts b/tests/corpus/5954-generic-optional-returns.ts index 9aaec5889..3f8309c9d 100644 --- a/tests/corpus/5954-generic-optional-returns.ts +++ b/tests/corpus/5954-generic-optional-returns.ts @@ -49,3 +49,18 @@ function second(rows: T[][], row: number, column: number): T { return rows[row][column]; } console.log(second([[1, 2]], 0, 1), second([[1, 2]], 0, 9), second(pairs, 0, 1)); + +// An async generic function's promise is always present; its settled +// value is undefined for a missing element. +async function firstAsync(items: T[]): Promise { + return items[0]; +} +const lookupAsync = async (table: Record, key: string): Promise => table[key]; +console.log(await firstAsync([1, 2]), await firstAsync([]), (await firstAsync([])) === undefined); +console.log(await lookupAsync({ a: "A" }, "a"), await lookupAsync({ a: "A" }, "b")); +async function settle(): Promise { + const found = await firstAsync(["x"]); + const missing = await firstAsync([]); + console.log(found, missing, missing ?? "none"); +} +await settle(); From 1b40157fed505e821eb017a554827e3a4cc6b544 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:25:56 -0500 Subject: [PATCH 34/67] Keep pending match metadata in the array's metadata slot A pending match result now stores its shared match source as a dynamic handle in ScrArrExt.metadata, next to its start index, instead of in a separate field with its own release hook. Array teardown already releases metadata, so scr_arr_ext_free and the publish check keep their previous code, and a static hello world links the same array code as on main. --- packages/runtime/src/scr_array.c | 8 +- packages/runtime/src/scr_array.h | 24 +++--- packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_publish.c | 2 +- packages/runtime/src/scr_regex.c | 126 ++++++++++++++++------------- 5 files changed, 87 insertions(+), 74 deletions(-) diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index fa494d1e5..0c5b4acc2 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -41,7 +41,7 @@ static void scr_arr_oom(void) { * sparse entry, noncanonical property or metadata allocates it. */ static const ScrArrExt scr_arr_no_ext; -SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks = NULL; +SCR_TL bool (*scr_arr_match_copy)(ScrArr *a, ScrDyn *target) = NULL; #define SCR_AX(a) ((const ScrArrExt *)((a)->ext ? (a)->ext : &scr_arr_no_ext)) ScrArrExt *scr_arr_ext(ScrArr *a) { @@ -1070,7 +1070,6 @@ void scr_arr_trace_v(void *a0, ScrTraceVisit visit, void *ctx) { * arrays have none. */ static void scr_arr_ext_free(ScrArr *a) { ScrArrExt *x = a->ext; - if (x->match_source) scr_arr_match_hooks->release(x->match_source); scr_arr_sparse_free(x->sparse); for (size_t i = 0; i < x->prop_len; i++) free(x->props[i].key); free(x->props); @@ -4426,7 +4425,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { - if (SCR_AX(a)->match_source && scr_arr_match_hooks->copy_metadata(a, target)) return; + const ScrDyn *metadata = SCR_AX(a)->metadata; + if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE && + scr_arr_match_copy(a, target)) + return; if (!SCR_AX(a)->metadata) return; for (size_t i = 0; i < SCR_AX(a)->metadata->v.obj.len; i++) { ScrDynEntry *entry = &SCR_AX(a)->metadata->v.obj.entries[i]; diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h index 616aa4c75..5289f8c13 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -105,11 +105,11 @@ typedef struct ScrArrExt { struct ScrArrProp *props; /* noncanonical numeric properties */ size_t prop_len; size_t prop_cap; - struct ScrDyn *metadata; /* heterogeneous named properties of match results */ - /* A match result's metadata before its first use: the match's shared - * subject and regex (scr_regex.c builds `metadata` from them) and its - * UTF-16 start index. */ - struct ScrRegexMatchSource *match_source; + /* Heterogeneous named properties of match results. Until a result's + * metadata is first needed, this is the match source handle that the + * results of one exec or matchAll call share (SCR_DYNH_MATCH_SOURCE, + * scr_regex.c), and match_index is the result's UTF-16 start index. */ + struct ScrDyn *metadata; double match_index; } ScrArrExt; /* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read @@ -212,15 +212,11 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) { * the candidate-root buffer like every headered type. */ void scr_arr_release(ScrArr *a); void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); -/* The match-source operations, installed by scr_regex.c when it records a - * pending match source (ScrArrExt.match_source); this unit links without - * the regex runtime. copy_metadata writes the pending metadata into the - * dynamic copy or materializes it, answering whether it wrote. */ -typedef struct ScrArrMatchHooks { - void (*release)(struct ScrRegexMatchSource *src); - bool (*copy_metadata)(ScrArr *a, struct ScrDyn *target); -} ScrArrMatchHooks; -extern SCR_TL const ScrArrMatchHooks *scr_arr_match_hooks; +/* Installed by scr_regex.c when it records a pending match source; this + * unit links without the regex runtime. Writes the pending metadata of + * `a` into its dynamic copy or materializes it, answering whether it + * wrote. */ +extern SCR_TL bool (*scr_arr_match_copy)(ScrArr *a, struct ScrDyn *target); /* The array's out-of-line storage, allocated (zeroed) on first use. */ ScrArrExt *scr_arr_ext(ScrArr *a); /* Moves inline dense slots (kept in the header block of a small array) to diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 8ebe11265..3e9adbf88 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -171,6 +171,7 @@ typedef enum { SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */ SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ + SCR_DYNH_MATCH_SOURCE, /* a match result's pending metadata (scr_regex.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index a974e82ff..9dedc6844 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -188,7 +188,7 @@ static void scr_pub_slot(ScrPub *ctx, uint64_t slot, ScrPubFn fn) { void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem) { ScrArr *a = v; - if (a->ext && (a->ext->props || a->ext->metadata || a->ext->match_source)) { + if (a->ext && (a->ext->props || a->ext->metadata)) { scr_pub_refuse(ctx, "Cannot publish an array with non-index properties"); return; } diff --git a/packages/runtime/src/scr_regex.c b/packages/runtime/src/scr_regex.c index 40e843cac..97c924d6d 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -612,11 +612,13 @@ static ScrStr *scr_regex_span(const ScrRegexInput *input, int start, int end) { /* ── match metadata ───────────────────────────────────────────────────── */ -/* Results of one exec or matchAll call share their subject and regex. - * Each result records the source and its start index; the metadata - * object (index, input, groups) is built when a result is first - * converted to a dynamic value, so results read only through their - * elements never allocate it. */ +/* Results of one exec or matchAll call share their subject and regex + * through one match source handle, which each result's metadata slot + * holds with its start index until the metadata object (index, input, + * groups) is first needed: when the result is converted to a dynamic + * value or its groups are read. Results read only through their elements + * never allocate it, and releasing a result releases the handle with the + * rest of its metadata. */ typedef struct ScrRegexMatchSource { size_t rc; ScrStr *input; @@ -624,57 +626,59 @@ typedef struct ScrRegexMatchSource { ScrDyn *input_value; /* `input` as a dynamic value, built on first use */ } ScrRegexMatchSource; -static void scr_regex_materialize_metadata(ScrArr *a); static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target); -static ScrRegexMatchSource *scr_regex_source_new(ScrStr *s, ScrRegex *re) { - ScrRegexMatchSource *src = malloc(sizeof *src); - if (!src) scr_regex_oom(); - src->rc = 1; - src->input = scr_str_retain(s); - src->re = scr_regex_retain(re); - src->input_value = NULL; - return src; +static void *scr_regex_source_retain_v(void *p) { + ((ScrRegexMatchSource *)p)->rc++; + return p; } -static void scr_regex_source_release(ScrRegexMatchSource *src) { - if (!src || --src->rc != 0) return; +static void scr_regex_source_release_v(void *p) { + ScrRegexMatchSource *src = p; + if (--src->rc != 0) return; scr_str_release(src->input); scr_regex_release(src->re); scr_dyn_release(src->input_value); free(src); } -/* Records the match at UTF-16 `index` of `src` as the row's pending - * metadata. */ -static void scr_regex_defer_metadata(ScrArr *row, ScrRegexMatchSource *src, int index) { - static const ScrArrMatchHooks hooks = { scr_regex_source_release, scr_regex_copy_pending_metadata }; - scr_arr_match_hooks = &hooks; +/* The +1 match source handle of one exec or matchAll call. */ +static ScrDyn *scr_regex_source_new(ScrStr *s, ScrRegex *re) { + static const ScrDynHandleOps ops = { + "MatchSource", &scr_regex_source_retain_v, &scr_regex_source_release_v, NULL, NULL, NULL, NULL, + }; + scr_dyn_handle_install(SCR_DYNH_MATCH_SOURCE, &ops); + scr_arr_match_copy = &scr_regex_copy_pending_metadata; + ScrRegexMatchSource *src = malloc(sizeof *src); + if (!src) scr_regex_oom(); + src->rc = 0; + src->input = scr_str_retain(s); + src->re = scr_regex_retain(re); + src->input_value = NULL; + return scr_dyn_new_handle(src, SCR_DYNH_MATCH_SOURCE); +} + +/* Records the match at UTF-16 `index` as the row's pending metadata. */ +static void scr_regex_defer_metadata(ScrArr *row, ScrDyn *source, int index) { ScrArrExt *x = scr_arr_ext(row); - src->rc++; - x->match_source = src; + x->metadata = scr_dyn_retain(source); x->match_index = (double)index; } +/* The pending match source of a result, or NULL once its metadata exists. */ +static ScrRegexMatchSource *scr_regex_pending(const ScrArr *a) { + const ScrDyn *metadata = a->ext ? a->ext->metadata : NULL; + if (!metadata || metadata->kind != SCR_DYN_HANDLE || metadata->v.handle.tag != SCR_DYNH_MATCH_SOURCE) + return NULL; + return metadata->v.handle.ptr; +} + /* The subject as a dynamic value, shared by the source's results. +1. */ static ScrDyn *scr_regex_source_input(ScrRegexMatchSource *src) { if (!src->input_value) src->input_value = scr_dyn_new_str(src->input); return scr_dyn_retain(src->input_value); } -ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { - ScrArrExt *x = a->ext; - if (x && x->match_source && !x->metadata) { - if (key->len == 5 && memcmp(key->data, "index", 5) == 0) - return scr_dyn_new_num(x->match_index); - if (key->len == 5 && memcmp(key->data, "input", 5) == 0) - return scr_regex_source_input(x->match_source); - scr_regex_materialize_metadata(a); - } - ScrDyn *value = x && x->metadata ? scr_dyn_obj_get(x->metadata, key->data, key->len) : NULL; - return scr_dyn_retain(value ? value : scr_dyn_undefined()); -} - /* groups is a null-prototype object mapping each group name to its first * participating capture (the row's own string, shared) or undefined, and * undefined itself for a pattern without named groups. The row holds each @@ -700,34 +704,44 @@ static ScrDyn *scr_regex_groups_of(const ScrArr *a, uint8_t *bc) { } static void scr_regex_materialize_metadata(ScrArr *a) { - ScrArrExt *x = scr_arr_ext(a); - ScrRegexMatchSource *src = x->match_source; + ScrRegexMatchSource *src = scr_regex_pending(a); if (!src) return; - x->match_source = NULL; - if (!x->metadata) { - ScrDyn *props = scr_dyn_new_obj(); - x->metadata = props; - scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); - scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); - scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); - } - scr_regex_source_release(src); + ScrArrExt *x = a->ext; + ScrDyn *source = x->metadata; + ScrDyn *props = scr_dyn_new_obj(); + scr_dyn_obj_set(props, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_obj_set(props, "input", 5, scr_regex_source_input(src)); + scr_dyn_obj_set(props, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); + x->metadata = props; + scr_dyn_release(source); } static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target) { - ScrArrExt *x = scr_arr_ext(a); - ScrRegexMatchSource *src = x->match_source; + ScrRegexMatchSource *src = scr_regex_pending(a); if (target->kind != SCR_DYN_ARR || target->v.arr.properties || target->non_extensible || target->v.arr.frozen) { scr_regex_materialize_metadata(a); return false; } - scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(x->match_index)); + scr_dyn_arr_named_put(target, "index", 5, scr_dyn_new_num(a->ext->match_index)); scr_dyn_arr_named_put(target, "input", 5, scr_regex_source_input(src)); scr_dyn_arr_named_put(target, "groups", 6, scr_regex_groups_of(a, scr_regex_bc(src->re))); return true; } +ScrDyn *scr_regex_result_prop(ScrArr *a, ScrStr *key) { + ScrRegexMatchSource *src = scr_regex_pending(a); + if (src) { + if (key->len == 5 && memcmp(key->data, "index", 5) == 0) + return scr_dyn_new_num(a->ext->match_index); + if (key->len == 5 && memcmp(key->data, "input", 5) == 0) return scr_regex_source_input(src); + scr_regex_materialize_metadata(a); + } + ScrDyn *metadata = a->ext ? a->ext->metadata : NULL; + ScrDyn *value = metadata ? scr_dyn_obj_get(metadata, key->data, key->len) : NULL; + return scr_dyn_retain(value ? value : scr_dyn_undefined()); +} + /* ECMA-262 AdvanceStringIndex: past a whole surrogate pair under /u, one * code unit otherwise. ASCII cannot contain a surrogate pair. */ static int scr_advance(const ScrRegexInput *input, int i, bool unicode) { @@ -863,9 +877,9 @@ static ScrArr *scr_regex_exec_core(ScrStr *s, ScrRegex *re, uint64_t mask, bool out->filled = (uint32_t)filled; out->len = n; if (metadata) { - ScrRegexMatchSource *src = scr_regex_source_new(s, re); - scr_regex_defer_metadata(out, src, scr_capture_index(&input, capture[0])); - scr_regex_source_release(src); + ScrDyn *source = scr_regex_source_new(s, re); + scr_regex_defer_metadata(out, source, scr_capture_index(&input, capture[0])); + scr_dyn_release(source); } } scr_regex_input_dispose(&input); @@ -1015,7 +1029,7 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices } bool unicode = (re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0; int capture_count = lre_get_capture_count(bc); - ScrRegexMatchSource *src = metadata ? scr_regex_source_new(s, re) : NULL; + ScrDyn *source = metadata ? scr_regex_source_new(s, re) : NULL; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; @@ -1048,12 +1062,12 @@ static ScrArr *scr_regex_match_all_core(ScrStr *s, ScrRegex *re, ScrArr *indices while (filled < (size_t)capture_count && present[filled] == SCR_ARR_VALUE) filled++; row->filled = (uint32_t)filled; row->len = (size_t)capture_count; - if (src) scr_regex_defer_metadata(row, src, start); + if (source) scr_regex_defer_metadata(row, source, start); scr_arr_push_ref(out, row); if (indices != NULL) scr_arr_push_f64(indices, (double)start); pos = end == start ? scr_advance(&input, end, unicode) : end; } - scr_regex_source_release(src); + scr_dyn_release(source); scr_regex_input_dispose(&input); return out; /* +1 (possibly empty — Node's no-match drain is []) */ } From 550e00549c1e3bbec8127adf086a4ad89bd71236 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 18:40:52 -0500 Subject: [PATCH 35/67] Move callSiteReturnType into ir.ts for devirtualization Lowering may not import ir/validate.ts, and devirtualize.ts imported callSiteReturnType from it. The function only reads an IrFunction's types, so it moves next to typeEquals in ir.ts, which both the validator and lowering import. The Date formatter fence's hint now lists toUTCString(), which this branch lowers, so the date-formatters and stdlib-fence snapshots record the new hint. toString() and toLocaleString() still have no lowering, and the fixtures keep them. --- .../src/frontend/lowering/devirtualize.ts | 9 ++++-- packages/compiler/src/ir/ir.ts | 27 +++++++++++++++++ packages/compiler/src/ir/validate.ts | 29 +------------------ .../__snapshots__/date-formatters.ts.txt | 4 +-- .../harness/__snapshots__/stdlib-fence.ts.txt | 2 +- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index 04a53aeb1..b71080a3f 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -1,7 +1,12 @@ import * as ts from "../ts7/adapter.js"; -import { type IrExpr, type IrFunction, type IrType, typeEquals } from "../../ir/ir.js"; +import { + type IrExpr, + type IrFunction, + type IrType, + callSiteReturnType, + typeEquals, +} from "../../ir/ir.js"; import { everyStmtList, transformStmtList } from "../../ir/traverse.js"; -import { callSiteReturnType } from "../../ir/validate.js"; import type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 68a4776f5..12b02d139 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -857,6 +857,33 @@ export function typeKey(t: IrType): string { } } +/** The type a CALL SITE (or function value) receives from a module + * function: an async body's returnType is the promise's INNER type, so + * calls and closures over it see Promise; a generator body's is the + * TReturn channel, so call sites see the generator type. The one place + * the body/call-site split is spelled out, for the validator and lowering. */ +export function callSiteReturnType(fn: IrFunction): IrType { + if (fn.async && fn.generator !== undefined) { + return { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: fn.returnType, + nextT: fn.generator.nextT, + }; + } + if (fn.async) return { kind: "promise", inner: fn.returnType }; + if (fn.generator !== undefined) { + return { + kind: "generator", + yieldT: fn.generator.yieldT, + retT: fn.returnType, + nextT: fn.generator.nextT, + }; + } + return fn.returnType; +} + export function typeEquals(a: IrType, b: IrType): boolean { // Dispatch once: a chain of negative narrowing checks repeatedly retags // the remaining variants when this comparator runs in the native compiler. diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 979b70115..2526dafa1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -56,7 +56,7 @@ import { unionContainerArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES } from "./ir.js"; +import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -85,33 +85,6 @@ export interface IrValidationError { loc: SrcLoc; } -/** The type a CALL SITE (or function value) receives from a module - * function: an async body's returnType is the promise's INNER type, so - * calls and closures over it see Promise; a generator body's is the - * TReturn channel, so call sites see the generator type. The one place - * the body/call-site split is spelled out in the validator. */ -export function callSiteReturnType(fn: IrFunction): IrType { - if (fn.async && fn.generator !== undefined) { - return { - kind: "generator", - async: true, - yieldT: fn.generator.yieldT, - retT: fn.returnType, - nextT: fn.generator.nextT, - }; - } - if (fn.async) return { kind: "promise", inner: fn.returnType }; - if (fn.generator !== undefined) { - return { - kind: "generator", - yieldT: fn.generator.yieldT, - retT: fn.returnType, - nextT: fn.generator.nextT, - }; - } - return fn.returnType; -} - interface VirtualCallInfo { declared: boolean; hasOverride: boolean; diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..90928dae7 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the | ^~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet @@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o | ^~~~~~~~~~~~~~~~~~~~~ 6 | - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..95fbe1fff 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet From 3de2fb56a06fea1f28c37fbc53a134af4e347a42 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 19:28:55 -0500 Subject: [PATCH 36/67] Cancel running subtests when their parent test times out node:test changes, matching Node 24.22: - When a test's { timeout } elapses, its subtests that are still running are cancelled, the innermost first. Each reports its duration so far and fails with "test did not finish before its parent and was cancelled", before the parent's result. - A cancelled subtest's body keeps running unobserved: when it settles it reports nothing and leaves the runner's state alone. Before, it reported its result after the run's summary. The timeout-subtests fixture compares the results, counts and failing section with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + packages/runtime/src/scr_test.c | 28 +++++++++++++++++++ .../node-test/cases/timeout-subtests.ts | 23 +++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/fixtures/node-test/cases/timeout-subtests.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 6583a2873..77d3d9a84 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -3,3 +3,4 @@ - **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is. - **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`. - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. +- **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index cc52513a6..4217169b8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -125,6 +125,7 @@ struct ScrTestCtx { struct ScrMockTimers *mock_timers; /* t.mock.timers, made on first use; freed at exit */ bool passed; /* t.passed: set when the body passes, as Node's pass() does */ bool listed; /* a suite listed in the failing section (a failing root before) */ + double start_ms; /* when the test started running; NaN before */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -230,6 +231,7 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->plan = -1; t->timeout = INFINITY; t->concurrency = 1; + t->start_ms = NAN; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif @@ -597,6 +599,23 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { scr_test_report_result(c); } +/* A test that timed out (Node): its subtests that are still running are + * cancelled, innermost first, with their durations so far. Their bodies + * keep running unobserved and report nothing when they settle. */ +static void scr_test_cancel_running(ScrTestCtx *t) { + for (size_t i = 0; i < t->nchildren; i++) { + ScrTestCtx *c = t->children[i]; + if (c->is_suite || c->state != SCR_TS_PENDING || isnan(c->start_ms)) continue; + scr_test_cancel_running(c); + static const char msg[] = "test did not finish before its parent and was cancelled"; + scr_throw_str(scr_str_new(msg, sizeof msg - 1)); + c->err = scr_exc_take(); + c->dur_ms = scr_now_ms() - c->start_ms; + c->state = SCR_TS_CANCELLED; + scr_test_report_result(c); + } +} + static void scr_test_fail_unrun(ScrTestCtx *c, ScrCaught *err) { double start = scr_now_ms(); c->err = scr_caught_retain(err); @@ -689,6 +708,7 @@ static void scr_test_run_node(ScrTestCtx *t) { * afterEach chain. */ ScrTestCtx *prev_parent = scr_test_parent_get(); scr_test_parent_set(t); + t->start_ms = start; if (t->concurrency > 1) scr_test_defer++; bool hooked = scr_test_run_each_hooks(t, SCR_HOOK_BEFORE_EACH, true); /* A concurrent subtest resumes after its beforeEach hooks as many turns @@ -703,10 +723,18 @@ static void scr_test_run_node(ScrTestCtx *t) { failed = true; } else if (t->cb) { scr_test_call(t->cb, t->flags, t, t); + if (t->state != SCR_TS_PENDING) { + /* Its parent timed out and cancelled it while the body ran: the + * result is reported, and the runner has moved on. */ + if (scr_exc_pending()) scr_exc_clear(); + if (t->concurrency > 1) scr_test_defer--; + return; + } if (scr_exc_pending()) { t->err = scr_exc_take(); failed = true; } + if (failed && t->timed_out) scr_test_cancel_running(t); } /* Children a passing body registered without running (bare test() in * the body): run them now, before the parent completes — Node holds diff --git a/tests/fixtures/node-test/cases/timeout-subtests.ts b/tests/fixtures/node-test/cases/timeout-subtests.ts new file mode 100644 index 000000000..91a5417e7 --- /dev/null +++ b/tests/fixtures/node-test/cases/timeout-subtests.ts @@ -0,0 +1,23 @@ +// @exit: 1 +// A test that times out cancels its subtests that are still running, the +// innermost first, with "test did not finish before its parent and was +// cancelled". Subtests that finished keep their results, and the +// cancelled bodies report nothing when they settle later. +import { test } from "node:test"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +test("parent", { timeout: 40 }, async (t) => { + await t.test("finished first", () => {}); + await t.test("running", async (t) => { + await t.test("inner done", () => {}); + await t.test("inner running", async () => { + await sleep(300); + throw new Error("never reported"); + }); + }); +}); + +test("next", async () => { + await sleep(400); +}); From b6637c882d60fc36a381d9c275b381319ea1a889 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:21 -0700 Subject: [PATCH 37/67] Check record, array and string fields that construction can read early A field without an initializer that the constructor assigns only after handing `this` to other code is undefined in Node until that assignment. Class-typed fields in that position already take the undefined-armed deferred-init slot whose reads check it. Record, array and string fields kept a plain pointer slot, so a method the constructor calls first could read null and crash with a segfault (`this.internal.n` read from a method before `this.internal = ...`). Those field kinds now take the same checked slot when a method or accessor the constructor calls through `this` (transitively, over the class and its bases) reads them. Fields only exposed in other ways keep their plain slot, so constructors whose early calls never touch the field compile as before. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-classes.ts | 69 ++++++++++++++++++- ...class-field-read-before-assignment.ts.json | 6 ++ ...6191-class-field-read-before-assignment.ts | 44 ++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json create mode 100644 tests/corpus/6191-class-field-read-before-assignment.ts diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 39b3242d6..6dadb029d 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3608,11 +3608,26 @@ export function collectClassShapeInner( } if (escape.assigned && !inst) { + let earlyReads: Set | null = null; for (const f of fieldOrder) { if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue; if (paramProps.some((pp) => pp.name === f.name)) continue; const declared = fields.get(f.name); - if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime) + // Records, arrays and strings are pointer slots that start empty + // too, but only take the checked slot when a method the + // constructor calls actually reads them: their other early + // exposures keep the plain slot. + if ( + declared?.kind === "record" || + declared?.kind === "array" || + declared?.kind === "string" + ) { + earlyReads ??= constructorMethodReads(decl, base, ctor ?? undefined); + if (!earlyReads.has(f.name)) continue; + } else if ( + declared?.kind !== "object" || + lowerer.classes.get(declared.className)?.def.runtime + ) continue; const armed = lowerer.withUndefinedArm(declared); if (armed === null || armed.kind !== "union") continue; @@ -4838,6 +4853,58 @@ export function staticShadowBelow(info: ClassInfo, name: string): boolean { ); } +/** The `this` members read by the methods and accessors a constructor + * calls through `this` (transitively, over the class and its bases), + * outside nested functions. The constructor's own reads are not included: + * they run where the constructor's statements put them. */ +function constructorMethodReads( + decl: ts.ClassLikeDeclaration, + base: ClassInfo | null, + ctor: ts.ConstructorDeclaration | undefined, +): Set { + const reads = new Set(); + const visited = new Set(); + const bodies = (name: string): ts.Node[] => { + const out: ts.Node[] = []; + let owner: ts.ClassLikeDeclaration | undefined = decl; + for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) { + for (const member of owner.members) { + if ( + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + member.body && + (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) && + member.name.text === name + ) + out.push(member.body); + } + } + return out; + }; + const enter = (name: string): void => { + if (visited.has(name)) return; + visited.add(name); + for (const body of bodies(name)) scan(body, true); + }; + // Nested functions (callbacks handed elsewhere) are not followed: only + // the straight-line code of the constructor and the methods it calls. + const scan = (node: ts.Node, inMethod: boolean): void => { + ts.walkPreorder(node, (n) => { + if (n !== node && ts.isFunctionLike(n)) return "skip"; + if ( + ts.isPropertyAccessExpression(n) && + n.expression.kind === ts.SyntaxKind.ThisKeyword && + (ts.isIdentifier(n.name) || ts.isPrivateIdentifier(n.name)) + ) { + if (inMethod) reads.add(n.name.text); + enter(n.name.text); + } + return undefined; + }); + }; + if (ctor?.body) scan(ctor.body, false); + return reads; +} + /** The nearest GENERIC static declaration of `name` at/above `info` — * findStaticOn's twin over the genericStatics tables. */ export function findGenericStaticOn( diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json new file mode 100644 index 000000000..3813f38bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6191-class-field-read-before-assignment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6191-class-field-read-before-assignment.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6191-class-field-read-before-assignment.ts b/tests/corpus/6191-class-field-read-before-assignment.ts new file mode 100644 index 000000000..92cb75b02 --- /dev/null +++ b/tests/corpus/6191-class-field-read-before-assignment.ts @@ -0,0 +1,44 @@ +// Fields without initializers that a method reads while the constructor +// has not assigned them yet (the constructor calls the method first). +// Node reads undefined there, so member access on it throws a TypeError; +// after the assignment the same reads see the values. + +class Settings { + internal: { n: number }; + items: number[]; + label: string; + + constructor() { + console.log(this.describe()); + this.internal = { n: 1 }; + this.items = [1, 2]; + this.label = "ready"; + console.log(this.describe()); + } + + describe(): string { + let internal: string; + let items: string; + let label: string; + try { + internal = String(this.internal.n); + } catch (e) { + internal = e instanceof TypeError ? "TypeError" : "other"; + } + try { + items = String(this.items.length); + } catch (e) { + items = e instanceof TypeError ? "TypeError" : "other"; + } + try { + label = this.label.toUpperCase(); + } catch (e) { + label = e instanceof TypeError ? "TypeError" : "other"; + } + return `${internal} ${items} ${label}`; + } +} + +const settings = new Settings(); +settings.items.push(3); +console.log(settings.internal.n, settings.items.length, settings.label); From f8fa60afecbdf26a6a4154774a9b2e907b0ad0f6 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:21 -0700 Subject: [PATCH 38/67] Link the promise adapter when a boxed class instance holds a promise Boxing a class instance as a dynamic value (Object.keys, spread, inspection) boxes its fields, and a promise field goes through scr_dyn_new_promise_adapting in scr_async_dyn.c. The link-unit scan followed promises through records, arrays, unions and functions but not class instances, so `Object.keys(job)` over a class with an optional `Promise` field failed to link. The scan now follows a class instance's fields. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/ir/runtime-features.ts | 15 +++++++++++++-- .../6192-optional-promise-field-keys.ts.json | 6 ++++++ tests/corpus/6192-optional-promise-field-keys.ts | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json create mode 100644 tests/corpus/6192-optional-promise-field-keys.ts diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index d16edfabd..57331d830 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1,4 +1,4 @@ -import type { IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js"; +import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef } from "./ir.js"; import type { IrLibFn } from "./builtin-signatures.js"; import { isDirentShape, @@ -132,10 +132,11 @@ export function moduleRuntimeFeatures(mod: IrModule): RuntimeFeatures { return scanRuntimeFeatures(mod); } -/** The module's record shapes and unions by id. */ +/** The module's record shapes, unions and class layouts by id. */ interface TypeTables { records: Map; unions: Map; + classes: Map; } /** Whether boxing a value of type `t` as dyn boxes a promise, which goes @@ -166,6 +167,15 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set): boolean const def = tables.unions.get(t.unionId); return def?.arms.some((a) => boxesPromise(a, tables, seen)) ?? false; } + case "object": { + // A boxed class instance exposes its fields as dyn values (Object.keys, + // spread, inspection), boxing a promise any field holds. + const key = `class:${t.className}`; + if (seen.has(key)) return false; + seen.add(key); + const def = tables.classes.get(t.className); + return def?.fields.some((f) => boxesPromise(f.type, tables, seen)) ?? false; + } default: return false; } @@ -222,6 +232,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run (tables ??= { records: new Map((mod.records ?? []).map((r) => [r.id, r])), unions: new Map((mod.unions ?? []).map((u) => [u.id, u])), + classes: new Map((mod.classes ?? []).map((c) => [c.name, c])), }); const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json new file mode 100644 index 000000000..dbae046c7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6192-optional-promise-field-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6192-optional-promise-field-keys.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6192-optional-promise-field-keys.ts b/tests/corpus/6192-optional-promise-field-keys.ts new file mode 100644 index 000000000..98cec870c --- /dev/null +++ b/tests/corpus/6192-optional-promise-field-keys.ts @@ -0,0 +1,12 @@ +// Object.keys over an instance whose optional field holds a promise: +// boxing the instance links the promise adapter of the runtime. + +class Job { + done?: Promise; + name = "job"; +} + +const job = new Job(); +console.log(Object.keys(job).join(",")); +job.done = Promise.resolve(); +console.log(Object.keys(job).join(","), typeof job.done); From 20f15f89fdbab54dc12342a87d38893bb738d139 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 15:43:22 -0700 Subject: [PATCH 39/67] Let implicit JS instances that fall through return undefined A JavaScript function specialized per call site takes its declared return type from JSDoc. bcryptjs's `hash` and `compare` document `@returns {!Promise}` but return nothing when a callback is passed, so their instances ended with the "the checker proved every path returns" trap and every callback-style call aborted with SC9002. Non-generic JavaScript functions already widen such a return with an undefined arm; implicit instances now do the same. The widened `Promise | undefined` result is then awaited by TypeScript callers, which was refused: an await of a promise-of-dynamic or unit union now goes through the dynamic await, which adopts the promise or hops once for the unit, as Node does. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/generic-functions.ts | 11 ++++++++ .../src/frontend/lowering/lower-exprs.ts | 12 +++++++++ .../main.ts.json | 6 +++++ .../main.ts | 9 +++++++ .../node_modules/callback-hash/index.d.ts | 6 +++++ .../node_modules/callback-hash/index.js | 26 +++++++++++++++++++ .../node_modules/callback-hash/package.json | 1 + 7 files changed, 71 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js create mode 100644 tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index bd785a256..11265be8b 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -13,6 +13,7 @@ import { } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; import { typeKey } from "../type-mapper.js"; +import { functionCanReturnUndefined } from "../function-completion.js"; import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js"; @@ -1428,6 +1429,16 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | return null; if (mapped !== null && promiseCarriesDyn(lowerer, mapped) && !hasExplicitJsDocReturn(info.decl)) return null; + // A JavaScript body that can complete without returning yields + // undefined there, whatever its JSDoc claims (bcryptjs's `hash` + // returns a promise only when no callback is passed), exactly as + // declaredReturnType widens non-generic JavaScript functions. + if ( + mapped !== null && + isJsSourceFile(info.decl.getSourceFile()) && + functionCanReturnUndefined(info.decl) + ) + return lowerer.withUndefinedArmOf(mapped) ?? mapped; return mapped; } catch (e) { if (!(e instanceof PoisonError)) throw e; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index c54341df8..6ec10e8d4 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1722,6 +1722,18 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const promiseArm = def.arms[promiseTag]!; const inner = promiseArm.kind === "promise" ? promiseArm.inner : VOID; const units = def.arms.filter(isUnitType); + // A checked-dynamic inner: the whole operand is a dyn value (the + // promise boxes by reference, the unit as itself), and the dyn + // await adopts the promise or hops once for the unit. + if (inner.kind === "dyn" && lowerer.dynConvertible(value.type)) { + return { + kind: "libCall", + fn: "async.awaitDyn", + args: [lowerer.coerceToExpected(value, DYN)], + type: DYN, + loc, + }; + } if (inner.kind === "union" || inner.kind === "dyn") { // A union inner would need an arm-wise re-tag into the result diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json new file mode 100644 index 000000000..39de1b842 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts new file mode 100644 index 000000000..fb61368f7 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/main.ts @@ -0,0 +1,9 @@ +// A package function whose JSDoc promises a Promise but that returns +// nothing when a callback is passed (bcryptjs's hash and compare). The +// call that falls through completes normally, as in Node. +import { hash } from "callback-hash"; + +hash("pw", 4, (err, value) => { + console.log("callback", err, value); +}); +console.log("promise", await hash("pw", "salt")); diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts new file mode 100644 index 000000000..819294565 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.d.ts @@ -0,0 +1,6 @@ +export declare function hash(input: string, salt: number | string): Promise; +export declare function hash( + input: string, + salt: number | string, + callback: (err: Error | null, value?: string) => void, +): void; diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js new file mode 100644 index 000000000..2661ea6ab --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/index.js @@ -0,0 +1,26 @@ +/** + * @param {string} input + * @param {number|string} salt + * @param {function(Error, string=)=} callback + * @param {function(number)=} progress + * @returns {!Promise} + */ +export function hash(input, salt, callback, progress) { + function run(done) { + if (progress) progress(1); + done(null, `${input}:${salt}`); + } + if (callback) { + if (typeof callback !== "function") throw Error("Illegal callback: " + typeof callback); + run(callback); + } else + return new Promise(function (resolve, reject) { + run(function (err, res) { + if (err) { + reject(err); + return; + } + resolve(res); + }); + }); +} diff --git a/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json new file mode 100644 index 000000000..d3553db20 --- /dev/null +++ b/tests/corpus/6190-npm-jsdoc-promise-fallthrough/node_modules/callback-hash/package.json @@ -0,0 +1 @@ +{ "name": "callback-hash", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } From 705c197a713407c19d986656cbe65df3b17ff1fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:20:05 -0500 Subject: [PATCH 40/67] Format Dates with Node's local time zone names Date.prototype.toString, toDateString and toTimeString, String(date), template literals and string concatenation print local time with the GMT offset and the zone's long name, as Node does. The runtime identifies the host zone from TZ or /etc/localtime, again after each write to process.env.TZ, and looks its name up in a table generated from Node's ICU data. Dates held in untyped values convert to strings with toString() and to numbers with their time value. --- .../content/limitations/builtin-apis/date.mdx | 2 +- .../limitations/dates-and-timezones.mdx | 2 + .../compiler/src/backend/llvm/lib-shared.ts | 3 + .../src/frontend/lowering/builtins/dates.ts | 12 +- .../src/frontend/lowering/lower-exprs.ts | 4 + .../src/frontend/lowering/surfaces.ts | 19 + .../compiler/src/ir/builtin-signatures.ts | 6 + packages/compiler/surface-manifest.json | 19 + .../tests/corpus/5963-date-to-string.ts.json | 6 + .../5964-date-dynamic-conversions.js.json | 6 + packages/runtime/src/scr_json.c | 35 +- packages/runtime/src/scr_lib.c | 381 +++- packages/runtime/src/scr_number.h | 9 + packages/runtime/src/scr_timezone_data.h | 1555 +++++++++++++++++ scripts/gen-timezone-names.mjs | 292 ++++ tests/corpus/5963-date-to-string.ts | 93 + tests/corpus/5964-date-dynamic-conversions.js | 29 + tests/diagnostics/date-formatters.ts | 2 +- .../__snapshots__/date-formatters.ts.txt | 12 +- .../harness/__snapshots__/stdlib-fence.ts.txt | 2 +- tests/harness/corpus-costs.json | 4 + 21 files changed, 2476 insertions(+), 17 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json create mode 100644 packages/runtime/src/scr_timezone_data.h create mode 100644 scripts/gen-timezone-names.mjs create mode 100644 tests/corpus/5963-date-to-string.ts create mode 100644 tests/corpus/5964-date-dynamic-conversions.js diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index db5278c53..81d37ebc3 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1 +1 @@ -- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. +- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, `toUTCString`, and locale formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`. See [Dates and timezones](#dates-and-timezones) for parsing restrictions. diff --git a/docs/content/limitations/dates-and-timezones.mdx b/docs/content/limitations/dates-and-timezones.mdx index ad830eb9a..e88cfb462 100644 --- a/docs/content/limitations/dates-and-timezones.mdx +++ b/docs/content/limitations/dates-and-timezones.mdx @@ -1,3 +1,5 @@ String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms. Local calendar getters use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule. + +`toString`, `toTimeString`, `String(date)` and template literals print the zone's long name after the GMT offset, as Node does (`GMT-0500 (Central Daylight Time)`). scriptc identifies the zone the way Node does, from `TZ` when it names a zone, else from `/etc/localtime`. Outside Windows, a write to `process.env.TZ` changes the zone for later conversions and getters. The names are the en-US names for the IANA zones; Node prints them in the language of the locale that `LC_ALL`, `LC_MESSAGES` or `LANG` selects, so with a non-English locale the name differs from Node's. A `TZ` value that is a file path or a POSIX rule string (such as `EST5EDT,M3.2.0,M11.1.0`) gives offsets and names that differ from Node's, which ignores those forms. On Windows, the name is the `GMT+hh:mm` form unless `TZ` names a zone. On `wasm32-wasi`, local time is UTC and the name is `Coordinated Universal Time`. diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 67cb77047..87acd391d 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -325,6 +325,9 @@ export const LIB_FN_SYMS: Record = { "date.getMilliseconds": "scr_date_get_milliseconds", "date.getUTCMilliseconds": "scr_date_get_milliseconds", "date.getTimezoneOffset": "scr_date_get_timezone_offset", + "date.toString": "scr_date_to_string", + "date.toDateString": "scr_date_to_date_string", + "date.toTimeString": "scr_date_to_time_string", "fs.existsSync": "scr_fs_exists", // ── the throwing slice (MAY_THROW_LIB_FNS members): the generic path // emits the standard pending check after each — exprs.ts's finish. diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index fa880bdc9..896eeaf28 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -33,8 +33,14 @@ const DATE_GETTER_FNS: Readonly> = { getTimezoneOffset: "date.getTimezoneOffset", }; +const DATE_TEXT_FNS: ReadonlyMap = new Map([ + ["toString", "date.toString"], + ["toDateString", "date.toDateString"], + ["toTimeString", "date.toTimeString"], +]); + const DATE_METHOD_HINT = - "getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering"; + "getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one * TimeClip'd millisecond scalar. Construction lives in lowerNew; @@ -117,6 +123,10 @@ export function lowerDateCall( if (fn !== undefined) { return { kind: "libCall", fn, args: [receiver], type: F64, loc }; } + const textFn = DATE_TEXT_FNS.get(name); + if (textFn !== undefined) { + return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + } lowerer.noLowering( `Date.prototype.${name}`, call, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d72faa936..a19c8e4ae 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -8863,6 +8863,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr if (e.type.kind === "string") return e; if (e.type.kind === "url") return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc }; + // String(date) / `${date}` / "" + date: Date's ToPrimitive prefers + // toString(). + if (e.type.kind === "date") + return { kind: "libCall", fn: "date.toString", args: [e], type: STRING, loc: e.loc }; if (e.type.kind === "regex") { return { kind: "regexIntrinsic", diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 44ed37e3c..0dead1c35 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2321,6 +2321,25 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ name: "Date.prototype.getTimezoneOffset", fns: ["date.getTimezoneOffset"], }, + { + id: "stdlib.date.toString", + kind: "stdlib", + name: "Date.prototype.toString", + fns: ["date.toString"], + note: "local time with the host zone's long English name; String(date) and template literals use it", + }, + { + id: "stdlib.date.toDateString", + kind: "stdlib", + name: "Date.prototype.toDateString", + fns: ["date.toDateString"], + }, + { + id: "stdlib.date.toTimeString", + kind: "stdlib", + name: "Date.prototype.toTimeString", + fns: ["date.toTimeString"], + }, // ── perf_hooks (lowerPerfHooksCall): the monotonic clock. { id: "node-builtin.perf_hooks.performance.now", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index e61bb2594..a87e40659 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -4208,6 +4208,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "date.getMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 }, "date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 }, + /** Date.prototype.toString / toDateString / toTimeString and String(date): + * V8's local-time formats with the host zone's long name, "Invalid Date" + * for an invalid date. Owned (+1) string; never throws. */ + "date.toString": { argTypes: [DATE_T], result: STRING }, + "date.toDateString": { argTypes: [DATE_T], result: STRING }, + "date.toTimeString": { argTypes: [DATE_T], result: STRING }, /** The composed `new Date(dateString).getTime()` read: one borrowed * string, f64 milliseconds since epoch. The parsed grammar is BOUNDED * (documented divergence — V8's parser accepts far more): the ASN.1 diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index a167254c2..637ecc2a0 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -4209,6 +4209,12 @@ "status": "static", "note": "one date string, using the bounded parser shared with new Date(dateString).getTime()" }, + { + "id": "stdlib.date.toDateString", + "kind": "stdlib", + "name": "Date.prototype.toDateString", + "status": "static" + }, { "id": "stdlib.date.toISOString", "kind": "stdlib", @@ -4216,6 +4222,19 @@ "status": "static", "note": "UTC ISO formatting over constructed and stored Date values" }, + { + "id": "stdlib.date.toString", + "kind": "stdlib", + "name": "Date.prototype.toString", + "status": "static", + "note": "local time with the host zone's long English name; String(date) and template literals use it" + }, + { + "id": "stdlib.date.toTimeString", + "kind": "stdlib", + "name": "Date.prototype.toTimeString", + "status": "static" + }, { "id": "stdlib.date.valueOf", "kind": "stdlib", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json new file mode 100644 index 000000000..b6743d677 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5963-date-to-string.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5963-date-to-string.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json new file mode 100644 index 000000000..1609e844d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5964-date-dynamic-conversions.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5964-date-dynamic-conversions.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 401933904..f1814e914 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6805,6 +6805,15 @@ static bool scr_dyn_error_encoding(const ScrDyn *d) { return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL; } +static bool scr_dyn_is_date(const ScrDyn *d) { + return d->kind == SCR_DYN_HANDLE && d->v.handle.tag == SCR_DYNH_DATE; +} + +/* A Date's valueOf(): its time value as a number. +1. */ +static ScrDyn *scr_dyn_date_value_of(const ScrDyn *d) { + return scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "valueOf", NULL, 0, "valueOf"); +} + /* Receiver-kind-dispatched toString() on a checked-dynamic value (the * dyn method surface — a stream's 'data'/for-await chunk is the common * receiver): bytes decode per the encoding (Node's Buffer.toString, @@ -6843,7 +6852,7 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { if (scr_dyn_error_encoding(d) && !scr_dyn_property_owner(d, "toString", 8)) return scr_dyn_error_string(d); return scr_str_new("[object Object]", 15); case SCR_DYN_HANDLE: - if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL) { + if (d->v.handle.tag == SCR_DYNH_REGEXP || d->v.handle.tag == SCR_DYNH_URL || d->v.handle.tag == SCR_DYNH_DATE) { ScrDyn *text = scr_dyn_handle_ops_of(d)->invoke(d->v.handle.ptr, (ScrDyn *)d, "toString", NULL, 0, "RegExp.toString"); if (!text) return NULL; ScrStr *out = scr_str_retain(text->v.str); @@ -7267,6 +7276,14 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { return false; } default: { + /* A Date's valueOf answers its time value. */ + if (scr_dyn_is_date(d)) { + ScrDyn *time = scr_dyn_date_value_of(d); + if (!time) return false; + *out = time->v.num; + scr_dyn_release(time); + return true; + } /* Arrays, byte views, functions, promises, and native handles inherit a * valueOf that returns the receiver, then use their existing JS-exact * string rendering for the numeric conversion. */ @@ -7287,8 +7304,14 @@ double scr_dyn_number_coerce(const ScrDyn *d) { static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d); +/* ToPrimitive with the number hint, which differs from the default hint + * only for a Date: its time value. */ +static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) { + return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d); +} + double scr_dyn_number_constructor(const ScrDyn *d) { - ScrDyn *primitive = scr_dyn_add_primitive(d); + ScrDyn *primitive = scr_dyn_number_primitive(d); if (!primitive) return NAN; double result = primitive->kind == SCR_DYN_BIGINT ? scr_bigint_to_f64(primitive->v.bigint) : scr_dyn_number_coerce(primitive); @@ -7297,7 +7320,7 @@ double scr_dyn_number_constructor(const ScrDyn *d) { } static ScrBigInt *scr_dyn_bigint_convert(const ScrDyn *d, bool allow_number) { - ScrDyn *primitive = scr_dyn_add_primitive(d); + ScrDyn *primitive = scr_dyn_number_primitive(d); if (!primitive) return NULL; ScrBigInt *result = NULL; switch (primitive->kind) { @@ -7430,7 +7453,7 @@ ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right) { } ScrDyn *scr_dyn_to_numeric(const ScrDyn *value) { - ScrDyn *primitive = scr_dyn_add_primitive(value); + ScrDyn *primitive = scr_dyn_number_primitive(value); if (!primitive) return NULL; if (primitive->kind == SCR_DYN_BIGINT) return primitive; double number; @@ -7454,9 +7477,9 @@ ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment) { bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation) { // ToPrimitive is observable and always visits the original left operand // first, including the > and <= spellings of relational comparison. - ScrDyn *l = scr_dyn_add_primitive(left); + ScrDyn *l = scr_dyn_number_primitive(left); if (!l) return false; - ScrDyn *r = scr_dyn_add_primitive(right); + ScrDyn *r = scr_dyn_number_primitive(right); if (!r) { scr_dyn_release(l); return false; } double order = 2; if (l->kind == SCR_DYN_STR && r->kind == SCR_DYN_STR) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..1d45de4d4 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10682,6 +10682,9 @@ double scr_date_utc(double y, double mo, double d, typedef struct ScrDateParts { long long year; int month, date, day, hours, minutes, seconds, milliseconds; + /* Local fields: tm_isdst at the instant (or its surrogate). It fills the + * padding before timezone_offset, so the struct keeps its size. */ + bool isdst; double timezone_offset; } ScrDateParts; @@ -10718,6 +10721,7 @@ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) { out->seconds = (int)(msday / 1000 % 60); out->milliseconds = (int)(msday % 1000); out->timezone_offset = 0; + out->isdst = false; } static bool scr_date_utc_parts(double ms, ScrDateParts *out) { @@ -10801,6 +10805,7 @@ static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) { scr_date_utc_parts_unchecked(clipped + local_offset * 1000.0, out); double timezone_offset = trunc(-local_offset / 60.0); out->timezone_offset = timezone_offset == 0 ? 0 : timezone_offset; + out->isdst = tmv.tm_isdst > 0; return true; } @@ -10840,6 +10845,364 @@ double scr_date_get_timezone_offset(double ms) { return scr_date_local_parts(ms, &p) ? p.timezone_offset : NAN; } +/* ── Date time zone names ─────────────────────────────────────────────── + * Long time zone names for Date.prototype.toString and toTimeString. + * + * Node prints "GMT-0500 (Central Daylight Time)": the name comes from ICU + * for the host zone, in standard or daylight form. ICU identifies the host + * zone from TZ when it looks like a zone ID, else from /etc/localtime (the + * symlink's target, or the zoneinfo file with the same contents), else + * from the libc zone abbreviation. The runtime follows the same steps and + * looks the ID up in the names Node prints for each tzdata zone + * (scr_timezone_data.h, generated). An ID without a row prints Node's + * fallback for a zone without a name, "GMT+hh:mm" with the zone's standard + * offset. + * + * Whether an instant is in daylight time follows V8: instants before 1970 + * or after 2038-01-19 use the instant with the same month, day and time in + * an equivalent year between 2008 and 2037 (same leap-year status and + * weekday of January 1), and tm_isdst decides. A few zones carry Node's + * own daylight-time changes instead: those whose tzdata rules use a + * negative daylight saving (Europe/Dublin's winter time, Morocco's Ramadan + * time), where ICU's data calls the larger offset daylight time, and those + * Node shows in daylight time through 2038 (British Columbia's permanent + * UTC-7, which tzdata names standard time). */ +#include "scr_timezone_data.h" + +#define SCR_TZ_ROWS (sizeof scr_tz_names / sizeof scr_tz_names[0]) + +/* Text that no other code in this file uses lives in named arrays: string + * literals would join the merged read-only strings of every program that + * links this file. */ +static const char scr_tz_posix_ids[4][8] = { "PST8PDT", "MST7MDT", "CST6CDT", "EST5EDT" }; +static const char scr_tz_prefixes[2][7] = { "posix/", "right/" }; +static const char scr_tz_env_name[] = "TZ"; +static const char scr_tz_utc[] = "UTC"; +static const char scr_tz_localtime_path[] = "/etc/localtime"; +static const char scr_tz_zoneinfo_tail[] = "/zoneinfo/"; +static const char scr_tz_zoneinfo_dir[] = "/usr/share/zoneinfo/"; +static const char scr_tz_skipped[4][11] = { ".", "..", "posixrules", "localtime" }; +static const char scr_tz_fallback_format[] = "GMT%c%02lld:%02lld"; +static const char scr_date_invalid_text[] = "Invalid Date"; + +/* The thread's host zone, detected again after a write to process.env.TZ + * (scr_date_tz_generation). */ +typedef struct { + unsigned generation; + const ScrTzNames *row; /* NULL: no row, use the fallback */ + bool resolved; + bool fallback_ready; + char fallback[16]; /* "GMT+hh:mm" */ +} ScrTzState; + +static SCR_TL ScrTzState scr_tz_state; + +/* The local UTC offset (seconds) and tm_isdst at an epoch second. */ +static bool scr_tz_offset_at(double sec, double *offset, bool *isdst) { + struct tm tmv; + if (!scr_date_localtime(sec, &tmv)) return false; + double local = + scr_days_from_civil((long long)tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday) * 86400.0 + + tmv.tm_hour * 3600.0 + tmv.tm_min * 60.0 + tmv.tm_sec; + *offset = local - sec; + *isdst = tmv.tm_isdst > 0; + return true; +} + +/* ICU's isValidOlsonID: letters and punctuation up to an optional run of at + * most two digits ending the string ("GMT+11"), or one of the four POSIX + * zone IDs tzdata also names. */ +static bool scr_tz_valid_id(const char *id) { + size_t i = 0; + while (id[i] && !(id[i] >= '0' && id[i] <= '9') && id[i] != ',') i++; + size_t max = i + 2; + while (id[i] && id[i] >= '0' && id[i] <= '9' && i < max) i++; + if (id[i] == 0) return true; + for (size_t k = 0; k < 4; k++) { + if (!strcmp(id, scr_tz_posix_ids[k])) return true; + } + return false; +} + +static const char *scr_tz_skip_prefix(const char *id) { + return !strncmp(id, scr_tz_prefixes[0], 6) || !strncmp(id, scr_tz_prefixes[1], 6) ? id + 6 : id; +} + +static const ScrTzNames *scr_tz_find(const char *id) { + size_t lo = 0, hi = SCR_TZ_ROWS; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + int c = strcmp(id, scr_tz_ids + scr_tz_names[mid].id); + if (c == 0) return &scr_tz_names[mid]; + if (c < 0) hi = mid; + else lo = mid + 1; + } + return NULL; +} + +#if !defined(_WIN32) && !defined(__wasi__) + +/* The bytes of a file, or NULL. */ +static char *scr_tz_read_file(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + size_t cap = 4096, n = 0; + char *data = malloc(cap); + while (data) { + n += fread(data + n, 1, cap - n, f); + if (n < cap) break; + char *grown = realloc(data, cap * 2); + if (!grown) { + free(data); + data = NULL; + break; + } + data = grown; + cap *= 2; + } + fclose(f); + *len = n; + return data; +} + +static bool scr_tz_same_file(const char *path, const char *data, size_t len) { + struct stat st; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode) || (size_t)st.st_size != len) return false; + size_t other_len; + char *other = scr_tz_read_file(path, &other_len); + bool same = other && other_len == len && memcmp(other, data, len) == 0; + free(other); + return same; +} + +/* ICU's search for a zoneinfo file with /etc/localtime's contents, in + * directory order, skipping "posixrules" and "localtime". Writes the + * path below the zoneinfo directory into `out`. */ +static bool scr_tz_search(char *dir, size_t dir_len, const char *data, size_t len, char *out, size_t cap) { + DIR *d = opendir(dir); + if (!d) return false; + bool found = false; + struct dirent *entry; + while (!found && (entry = readdir(d)) != NULL) { + const char *name = entry->d_name; + bool skipped = false; + for (size_t k = 0; k < 4; k++) skipped = skipped || !strcmp(name, scr_tz_skipped[k]); + if (skipped) continue; + size_t name_len = strlen(name); + if (dir_len + name_len + 2 > PATH_MAX) continue; + memcpy(dir + dir_len, name, name_len + 1); + DIR *sub = opendir(dir); + if (sub) { + closedir(sub); + dir[dir_len + name_len] = '/'; + dir[dir_len + name_len + 1] = 0; + found = scr_tz_search(dir, dir_len + name_len + 1, data, len, out, cap); + } else if (scr_tz_same_file(dir, data, len)) { + const char *id = scr_tz_skip_prefix(dir + strlen(scr_tz_zoneinfo_dir)); + size_t id_len = strlen(id); + if (id_len < cap) { + memcpy(out, id, id_len + 1); + found = true; + } + } + dir[dir_len] = 0; + } + closedir(d); + return found; +} +#endif + +/* The host zone's row, or NULL when ICU would not find a zone name. */ +static const ScrTzNames *scr_tz_detect(void) { +#if defined(__wasi__) + /* WASI's libc keeps every local time in UTC. */ + return scr_tz_find(scr_tz_utc); +#else + const char *tz = getenv(scr_tz_env_name); + if (tz && scr_tz_valid_id(tz)) { + if (tz[0] == ':') tz++; + return scr_tz_find(scr_tz_skip_prefix(tz)); + } +#ifdef _WIN32 + return NULL; +#else + /* The zone ID is the path below a zoneinfo directory of the + * /etc/localtime symlink's target, or of a zoneinfo file with the same + * contents when /etc/localtime is a regular file. */ + char path[PATH_MAX]; + if (realpath(scr_tz_localtime_path, path) && strcmp(path, scr_tz_localtime_path) != 0) { + const char *tail = strstr(path, scr_tz_zoneinfo_tail); + if (tail) { + tail = scr_tz_skip_prefix(tail + strlen(scr_tz_zoneinfo_tail)); + if (scr_tz_valid_id(tail)) return scr_tz_find(tail); + } + } else { + size_t len; + DIR *zoneinfo = opendir(scr_tz_zoneinfo_dir); + char *data = zoneinfo ? scr_tz_read_file(scr_tz_localtime_path, &len) : NULL; + if (zoneinfo) closedir(zoneinfo); + if (data) { + char dir[PATH_MAX]; + memcpy(dir, scr_tz_zoneinfo_dir, sizeof scr_tz_zoneinfo_dir); + char id[PATH_MAX]; + bool found = scr_tz_search(dir, strlen(dir), data, len, id, sizeof id); + free(data); + if (found && scr_tz_valid_id(id)) return scr_tz_find(id); + } + } + /* Otherwise the standard-time abbreviation names the zone ("UTC" when + * there is no /etc/localtime). */ + tzset(); + return scr_tz_find(tzname[0]); +#endif +#endif +} + +static ScrTzState *scr_tz_current(void) { + ScrTzState *s = &scr_tz_state; + unsigned generation = scr_date_tz_generation; + if (s->generation != generation || !s->resolved) { + s->generation = generation; + s->row = scr_tz_detect(); + s->resolved = true; + s->fallback_ready = false; + } + return s; +} + +/* "GMT+hh:mm" for the zone's current standard offset: the name Node prints + * for a zone ID it does not know. */ +static const char *scr_tz_fallback(ScrTzState *s) { + if (!s->fallback_ready) { + double offset = 0; + bool isdst = false; + long long seconds = 0; + if (scr_tz_offset_at((double)time(NULL), &offset, &isdst)) seconds = (long long)offset - (isdst ? 3600 : 0); + long long minutes = (seconds < 0 ? -seconds : seconds) / 60; + snprintf(s->fallback, sizeof s->fallback, scr_tz_fallback_format, seconds < 0 ? '-' : '+', + minutes / 60, minutes % 60); + s->fallback_ready = true; + } + return s->fallback; +} + +/* V8's EquivalentTime: the same month, day and time of day in a year + * between 2008 and 2037 with the same leap-year status and weekday of + * January 1. */ +static double scr_tz_equivalent_time(double ms) { + ScrDateParts p; + scr_date_utc_parts_unchecked(ms, &p); + double within = ms - floor(ms / 86400000.0) * 86400000.0; + long long year = p.year; + bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + long long week_day = ((long long)scr_days_from_civil(year, 1, 1) + 4) % 7; + if (week_day < 0) week_day += 7; + long long recent = (leap ? 1956 : 1967) + (week_day * 12) % 28; + long long equivalent = 2008 + (recent + 3 * 28 - 2008) % 28; + return (scr_days_from_civil(equivalent, p.month + 1, 1) + p.date - 1) * 86400000.0 + within; +} + +static bool scr_tz_is_daylight(double ms, int isdst_hint, const ScrTzNames *row) { + /* V8 names instants in [0, 2^31 - 1] seconds directly. */ + if (ms < 0 || (long long)ms > 2147483647000LL) { + ms = scr_tz_equivalent_time(ms); + isdst_hint = -1; + } + double sec = floor(ms / 1000.0); + if (row->change_count > 0) { + /* Daylight time after an odd number of changes. */ + const int32_t *changes = scr_tz_daylight_changes + row->first_change; + size_t lo = 0, hi = row->change_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (changes[mid] <= sec) lo = mid + 1; + else hi = mid; + } + return (lo & 1) != 0; + } + if (isdst_hint >= 0) return isdst_hint != 0; + double offset; + bool isdst; + return scr_tz_offset_at(sec, &offset, &isdst) && isdst; +} + +const char *scr_date_zone_name(double ms, int isdst) { + ScrTzState *s = scr_tz_current(); + if (!s->row) return scr_tz_fallback(s); + bool daylight = scr_tz_is_daylight(ms, isdst, s->row); + return scr_tz_name_text + (daylight ? s->row->daylight : s->row->standard); +} + +/* Date.prototype.toString, toDateString and toTimeString: V8's formats over + * the local calendar fields ("Sat Oct 18 2026 09:12:00 GMT-0500 (Central + * Daylight Time)"), with years before 1 as "-0001", and "Invalid Date" for + * an invalid date. */ +enum { SCR_DATE_TEXT_FULL, SCR_DATE_TEXT_DATE, SCR_DATE_TEXT_TIME }; + +/* `value` (>= 0) in decimal, zero-padded to `width` digits. */ +static char *scr_date_put_digits(char *p, long long value, int width) { + char digits[24]; + int n = 0; + do { + digits[n++] = (char)('0' + value % 10); + value /= 10; + } while (value > 0); + while (n < width) digits[n++] = '0'; + while (n > 0) *p++ = digits[--n]; + return p; +} + +static ScrStr *scr_date_text(double ms, int mode) { + static const char week_days[7][3] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + static const char months[12][3] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + ScrDateParts p; + if (!scr_date_local_parts(ms, &p)) return scr_str_new(scr_date_invalid_text, sizeof scr_date_invalid_text - 1); + char buf[160]; + char *o = buf; + if (mode != SCR_DATE_TEXT_TIME) { + memcpy(o, week_days[p.day], 3); + o[3] = ' '; + memcpy(o + 4, months[p.month], 3); + o[7] = ' '; + o = scr_date_put_digits(o + 8, p.date, 2); + *o++ = ' '; + /* "%04d", or "%05d" (a sign and four digits) before year 0. */ + if (p.year < 0) *o++ = '-'; + o = scr_date_put_digits(o, p.year < 0 ? -p.year : p.year, 4); + } + if (mode == SCR_DATE_TEXT_FULL) *o++ = ' '; + if (mode != SCR_DATE_TEXT_DATE) { + int offset = -(int)p.timezone_offset; + int magnitude = offset < 0 ? -offset : offset; + o = scr_date_put_digits(o, p.hours, 2); + *o++ = ':'; + o = scr_date_put_digits(o, p.minutes, 2); + *o++ = ':'; + o = scr_date_put_digits(o, p.seconds, 2); + *o++ = ' '; + *o++ = 'G'; + *o++ = 'M'; + *o++ = 'T'; + *o++ = offset < 0 ? '-' : '+'; + o = scr_date_put_digits(o, magnitude / 60, 2); + o = scr_date_put_digits(o, magnitude % 60, 2); + *o++ = ' '; + *o++ = '('; + /* Zone names are at most 64 bytes (scr_timezone_data.h). */ + const char *zone = scr_date_zone_name(ms, p.isdst); + size_t zone_len = strlen(zone); + memcpy(o, zone, zone_len); + o += zone_len; + *o++ = ')'; + } + return scr_str_new(buf, (size_t)(o - buf)); +} + +ScrStr *scr_date_to_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_FULL); } +ScrStr *scr_date_to_date_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_DATE); } +ScrStr *scr_date_to_time_string(double ms) { return scr_date_text(ms, SCR_DATE_TEXT_TIME); } + /* ── Number statics ──────────────────────────────────────────────────── * JS-exact: the ES2015 Number statics never coerce (unlike the global * isNaN/isFinite), and the compiler routes only number-typed arguments @@ -11176,11 +11539,25 @@ double scr_dyn_native_date_value(const ScrDyn *value) { return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; } +/* Method names without another use in this file, in named arrays (see + * scr_tz_posix_ids). */ +static const char scr_date_to_date_string_name[] = "toDateString"; +static const char scr_date_to_time_string_name[] = "toTimeString"; + static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { (void)self; (void)args; (void)argc; (void)what; double ms = ((ScrNativeDate *)ptr)->milliseconds; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); + if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || + !strcmp(method, scr_date_to_time_string_name)) { + ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms) + : !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms) + : scr_date_to_time_string(ms); + ScrDyn *result = scr_dyn_new_str(text); + scr_str_release(text); + return result; + } if (!strcmp(method, "toISOString") || !strcmp(method, "toJSON")) { if (!strcmp(method, "toJSON") && !isfinite(ms)) return scr_dyn_new_null(); ScrStr *text = scr_date_to_iso(ms); @@ -11228,7 +11605,9 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { - "getTime", "valueOf", "toISOString", "toJSON", "getFullYear", "getUTCFullYear", "getMonth", + "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, + scr_date_to_time_string_name, + "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", "getUTCMilliseconds", "getTimezoneOffset", diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f980daeb5..ac96db8e7 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -120,6 +120,15 @@ double scr_date_get_minutes(double ms, bool utc); double scr_date_get_seconds(double ms, bool utc); double scr_date_get_milliseconds(double ms); double scr_date_get_timezone_offset(double ms); +/* Date.prototype.toString, toDateString and toTimeString with V8's + * formats; "Invalid Date" for NaN. +1. */ +ScrStr *scr_date_to_string(double ms); +ScrStr *scr_date_to_date_string(double ms); +ScrStr *scr_date_to_time_string(double ms); +/* The long time zone name Node prints after the GMT offset for the host + * zone at time value `ms`; `isdst` is tm_isdst at `ms`, or -1 when not + * known. Borrowed; valid until the next call on the same thread. */ +const char *scr_date_zone_name(double ms, int isdst); /* One-argument ABI wrappers used by the LLVM lib-call table. */ double scr_date_get_full_year_local(double ms); double scr_date_get_full_year_utc(double ms); diff --git a/packages/runtime/src/scr_timezone_data.h b/packages/runtime/src/scr_timezone_data.h new file mode 100644 index 000000000..3503b84f9 --- /dev/null +++ b/packages/runtime/src/scr_timezone_data.h @@ -0,0 +1,1555 @@ +/* Generated by scripts/gen-timezone-names.mjs from Node v24.22.0 + * (ICU 78.3, CLDR 48.0) and tzdata 2026c. Do not edit. + * Each IANA zone or link ID maps to the long English names Node prints for + * its standard and daylight time. A row with change_count > 0 takes + * daylight time from scr_tz_daylight_changes[first_change ...]: the epoch + * seconds at which daylight time starts or ends between 1970 and + * 2038-01-19 (INT32_MIN first when 1970 starts in daylight time). Rows are + * sorted by ID. */ +#ifndef SCR_TIMEZONE_DATA_H +#define SCR_TIMEZONE_DATA_H +#include +typedef struct { uint16_t id, standard, daylight, first_change, change_count; } ScrTzNames; +static const char scr_tz_ids[] = + "Africa/Abidjan\0" + "Africa/Accra\0" + "Africa/Addis_Ababa\0" + "Africa/Algiers\0" + "Africa/Asmara\0" + "Africa/Asmera\0" + "Africa/Bamako\0" + "Africa/Bangui\0" + "Africa/Banjul\0" + "Africa/Bissau\0" + "Africa/Blantyre\0" + "Africa/Brazzaville\0" + "Africa/Bujumbura\0" + "Africa/Cairo\0" + "Africa/Casablanca\0" + "Africa/Ceuta\0" + "Africa/Conakry\0" + "Africa/Dakar\0" + "Africa/Dar_es_Salaam\0" + "Africa/Djibouti\0" + "Africa/Douala\0" + "Africa/El_Aaiun\0" + "Africa/Freetown\0" + "Africa/Gaborone\0" + "Africa/Harare\0" + "Africa/Johannesburg\0" + "Africa/Juba\0" + "Africa/Kampala\0" + "Africa/Khartoum\0" + "Africa/Kigali\0" + "Africa/Kinshasa\0" + "Africa/Lagos\0" + "Africa/Libreville\0" + "Africa/Lome\0" + "Africa/Luanda\0" + "Africa/Lubumbashi\0" + "Africa/Lusaka\0" + "Africa/Malabo\0" + "Africa/Maputo\0" + "Africa/Maseru\0" + "Africa/Mbabane\0" + "Africa/Mogadishu\0" + "Africa/Monrovia\0" + "Africa/Nairobi\0" + "Africa/Ndjamena\0" + "Africa/Niamey\0" + "Africa/Nouakchott\0" + "Africa/Ouagadougou\0" + "Africa/Porto-Novo\0" + "Africa/Sao_Tome\0" + "Africa/Timbuktu\0" + "Africa/Tripoli\0" + "Africa/Tunis\0" + "Africa/Windhoek\0" + "America/Adak\0" + "America/Anchorage\0" + "America/Anguilla\0" + "America/Antigua\0" + "America/Araguaina\0" + "America/Argentina/Buenos_Aires\0" + "America/Argentina/Catamarca\0" + "America/Argentina/ComodRivadavia\0" + "America/Argentina/Cordoba\0" + "America/Argentina/Jujuy\0" + "America/Argentina/La_Rioja\0" + "America/Argentina/Mendoza\0" + "America/Argentina/Rio_Gallegos\0" + "America/Argentina/Salta\0" + "America/Argentina/San_Juan\0" + "America/Argentina/San_Luis\0" + "America/Argentina/Tucuman\0" + "America/Argentina/Ushuaia\0" + "America/Aruba\0" + "America/Asuncion\0" + "America/Atikokan\0" + "America/Atka\0" + "America/Bahia\0" + "America/Bahia_Banderas\0" + "America/Barbados\0" + "America/Belem\0" + "America/Belize\0" + "America/Blanc-Sablon\0" + "America/Boa_Vista\0" + "America/Bogota\0" + "America/Boise\0" + "America/Buenos_Aires\0" + "America/Cambridge_Bay\0" + "America/Campo_Grande\0" + "America/Cancun\0" + "America/Caracas\0" + "America/Catamarca\0" + "America/Cayenne\0" + "America/Cayman\0" + "America/Chicago\0" + "America/Chihuahua\0" + "America/Ciudad_Juarez\0" + "America/Coral_Harbour\0" + "America/Cordoba\0" + "America/Costa_Rica\0" + "America/Coyhaique\0" + "America/Creston\0" + "America/Cuiaba\0" + "America/Curacao\0" + "America/Danmarkshavn\0" + "America/Dawson\0" + "America/Dawson_Creek\0" + "America/Denver\0" + "America/Detroit\0" + "America/Dominica\0" + "America/Edmonton\0" + "America/Eirunepe\0" + "America/El_Salvador\0" + "America/Ensenada\0" + "America/Fort_Nelson\0" + "America/Fort_Wayne\0" + "America/Fortaleza\0" + "America/Glace_Bay\0" + "America/Godthab\0" + "America/Goose_Bay\0" + "America/Grand_Turk\0" + "America/Grenada\0" + "America/Guadeloupe\0" + "America/Guatemala\0" + "America/Guayaquil\0" + "America/Guyana\0" + "America/Halifax\0" + "America/Havana\0" + "America/Hermosillo\0" + "America/Indiana/Indianapolis\0" + "America/Indiana/Knox\0" + "America/Indiana/Marengo\0" + "America/Indiana/Petersburg\0" + "America/Indiana/Tell_City\0" + "America/Indiana/Vevay\0" + "America/Indiana/Vincennes\0" + "America/Indiana/Winamac\0" + "America/Indianapolis\0" + "America/Inuvik\0" + "America/Iqaluit\0" + "America/Jamaica\0" + "America/Jujuy\0" + "America/Juneau\0" + "America/Kentucky/Louisville\0" + "America/Kentucky/Monticello\0" + "America/Knox_IN\0" + "America/Kralendijk\0" + "America/La_Paz\0" + "America/Lima\0" + "America/Los_Angeles\0" + "America/Louisville\0" + "America/Lower_Princes\0" + "America/Maceio\0" + "America/Managua\0" + "America/Manaus\0" + "America/Marigot\0" + "America/Martinique\0" + "America/Matamoros\0" + "America/Mazatlan\0" + "America/Mendoza\0" + "America/Menominee\0" + "America/Merida\0" + "America/Metlakatla\0" + "America/Mexico_City\0" + "America/Miquelon\0" + "America/Moncton\0" + "America/Monterrey\0" + "America/Montevideo\0" + "America/Montreal\0" + "America/Montserrat\0" + "America/Nassau\0" + "America/New_York\0" + "America/Nipigon\0" + "America/Nome\0" + "America/Noronha\0" + "America/North_Dakota/Beulah\0" + "America/North_Dakota/Center\0" + "America/North_Dakota/New_Salem\0" + "America/Nuuk\0" + "America/Ojinaga\0" + "America/Panama\0" + "America/Pangnirtung\0" + "America/Paramaribo\0" + "America/Phoenix\0" + "America/Port-au-Prince\0" + "America/Port_of_Spain\0" + "America/Porto_Acre\0" + "America/Porto_Velho\0" + "America/Puerto_Rico\0" + "America/Punta_Arenas\0" + "America/Rainy_River\0" + "America/Rankin_Inlet\0" + "America/Recife\0" + "America/Regina\0" + "America/Resolute\0" + "America/Rio_Branco\0" + "America/Rosario\0" + "America/Santa_Isabel\0" + "America/Santarem\0" + "America/Santiago\0" + "America/Santo_Domingo\0" + "America/Sao_Paulo\0" + "America/Scoresbysund\0" + "America/Shiprock\0" + "America/Sitka\0" + "America/St_Barthelemy\0" + "America/St_Johns\0" + "America/St_Kitts\0" + "America/St_Lucia\0" + "America/St_Thomas\0" + "America/St_Vincent\0" + "America/Swift_Current\0" + "America/Tegucigalpa\0" + "America/Thule\0" + "America/Thunder_Bay\0" + "America/Tijuana\0" + "America/Toronto\0" + "America/Tortola\0" + "America/Vancouver\0" + "America/Virgin\0" + "America/Whitehorse\0" + "America/Winnipeg\0" + "America/Yakutat\0" + "America/Yellowknife\0" + "Antarctica/Casey\0" + "Antarctica/Davis\0" + "Antarctica/DumontDUrville\0" + "Antarctica/Macquarie\0" + "Antarctica/Mawson\0" + "Antarctica/McMurdo\0" + "Antarctica/Palmer\0" + "Antarctica/Rothera\0" + "Antarctica/South_Pole\0" + "Antarctica/Syowa\0" + "Antarctica/Troll\0" + "Antarctica/Vostok\0" + "Arctic/Longyearbyen\0" + "Asia/Aden\0" + "Asia/Almaty\0" + "Asia/Amman\0" + "Asia/Anadyr\0" + "Asia/Aqtau\0" + "Asia/Aqtobe\0" + "Asia/Ashgabat\0" + "Asia/Ashkhabad\0" + "Asia/Atyrau\0" + "Asia/Baghdad\0" + "Asia/Bahrain\0" + "Asia/Baku\0" + "Asia/Bangkok\0" + "Asia/Barnaul\0" + "Asia/Beirut\0" + "Asia/Bishkek\0" + "Asia/Brunei\0" + "Asia/Calcutta\0" + "Asia/Chita\0" + "Asia/Choibalsan\0" + "Asia/Chongqing\0" + "Asia/Chungking\0" + "Asia/Colombo\0" + "Asia/Dacca\0" + "Asia/Damascus\0" + "Asia/Dhaka\0" + "Asia/Dili\0" + "Asia/Dubai\0" + "Asia/Dushanbe\0" + "Asia/Famagusta\0" + "Asia/Gaza\0" + "Asia/Harbin\0" + "Asia/Hebron\0" + "Asia/Ho_Chi_Minh\0" + "Asia/Hong_Kong\0" + "Asia/Hovd\0" + "Asia/Irkutsk\0" + "Asia/Istanbul\0" + "Asia/Jakarta\0" + "Asia/Jayapura\0" + "Asia/Jerusalem\0" + "Asia/Kabul\0" + "Asia/Kamchatka\0" + "Asia/Karachi\0" + "Asia/Kashgar\0" + "Asia/Kathmandu\0" + "Asia/Katmandu\0" + "Asia/Khandyga\0" + "Asia/Kolkata\0" + "Asia/Krasnoyarsk\0" + "Asia/Kuala_Lumpur\0" + "Asia/Kuching\0" + "Asia/Kuwait\0" + "Asia/Macao\0" + "Asia/Macau\0" + "Asia/Magadan\0" + "Asia/Makassar\0" + "Asia/Manila\0" + "Asia/Muscat\0" + "Asia/Nicosia\0" + "Asia/Novokuznetsk\0" + "Asia/Novosibirsk\0" + "Asia/Omsk\0" + "Asia/Oral\0" + "Asia/Phnom_Penh\0" + "Asia/Pontianak\0" + "Asia/Pyongyang\0" + "Asia/Qatar\0" + "Asia/Qostanay\0" + "Asia/Qyzylorda\0" + "Asia/Rangoon\0" + "Asia/Riyadh\0" + "Asia/Saigon\0" + "Asia/Sakhalin\0" + "Asia/Samarkand\0" + "Asia/Seoul\0" + "Asia/Shanghai\0" + "Asia/Singapore\0" + "Asia/Srednekolymsk\0" + "Asia/Taipei\0" + "Asia/Tashkent\0" + "Asia/Tbilisi\0" + "Asia/Tehran\0" + "Asia/Tel_Aviv\0" + "Asia/Thimbu\0" + "Asia/Thimphu\0" + "Asia/Tokyo\0" + "Asia/Tomsk\0" + "Asia/Ujung_Pandang\0" + "Asia/Ulaanbaatar\0" + "Asia/Ulan_Bator\0" + "Asia/Urumqi\0" + "Asia/Ust-Nera\0" + "Asia/Vientiane\0" + "Asia/Vladivostok\0" + "Asia/Yakutsk\0" + "Asia/Yangon\0" + "Asia/Yekaterinburg\0" + "Asia/Yerevan\0" + "Atlantic/Azores\0" + "Atlantic/Bermuda\0" + "Atlantic/Canary\0" + "Atlantic/Cape_Verde\0" + "Atlantic/Faeroe\0" + "Atlantic/Faroe\0" + "Atlantic/Jan_Mayen\0" + "Atlantic/Madeira\0" + "Atlantic/Reykjavik\0" + "Atlantic/South_Georgia\0" + "Atlantic/St_Helena\0" + "Atlantic/Stanley\0" + "Australia/ACT\0" + "Australia/Adelaide\0" + "Australia/Brisbane\0" + "Australia/Broken_Hill\0" + "Australia/Canberra\0" + "Australia/Currie\0" + "Australia/Darwin\0" + "Australia/Eucla\0" + "Australia/Hobart\0" + "Australia/LHI\0" + "Australia/Lindeman\0" + "Australia/Lord_Howe\0" + "Australia/Melbourne\0" + "Australia/NSW\0" + "Australia/North\0" + "Australia/Perth\0" + "Australia/Queensland\0" + "Australia/South\0" + "Australia/Sydney\0" + "Australia/Tasmania\0" + "Australia/Victoria\0" + "Australia/West\0" + "Australia/Yancowinna\0" + "Brazil/Acre\0" + "Brazil/DeNoronha\0" + "Brazil/East\0" + "Brazil/West\0" + "CET\0" + "CST6CDT\0" + "Canada/Atlantic\0" + "Canada/Central\0" + "Canada/Eastern\0" + "Canada/Mountain\0" + "Canada/Newfoundland\0" + "Canada/Pacific\0" + "Canada/Saskatchewan\0" + "Canada/Yukon\0" + "Chile/Continental\0" + "Chile/EasterIsland\0" + "Cuba\0" + "EET\0" + "EST\0" + "EST5EDT\0" + "Egypt\0" + "Eire\0" + "Etc/GMT\0" + "Etc/GMT+0\0" + "Etc/GMT+1\0" + "Etc/GMT+10\0" + "Etc/GMT+11\0" + "Etc/GMT+12\0" + "Etc/GMT+2\0" + "Etc/GMT+3\0" + "Etc/GMT+4\0" + "Etc/GMT+5\0" + "Etc/GMT+6\0" + "Etc/GMT+7\0" + "Etc/GMT+8\0" + "Etc/GMT+9\0" + "Etc/GMT-0\0" + "Etc/GMT-1\0" + "Etc/GMT-10\0" + "Etc/GMT-11\0" + "Etc/GMT-12\0" + "Etc/GMT-13\0" + "Etc/GMT-14\0" + "Etc/GMT-2\0" + "Etc/GMT-3\0" + "Etc/GMT-4\0" + "Etc/GMT-5\0" + "Etc/GMT-6\0" + "Etc/GMT-7\0" + "Etc/GMT-8\0" + "Etc/GMT-9\0" + "Etc/GMT0\0" + "Etc/Greenwich\0" + "Etc/UCT\0" + "Etc/UTC\0" + "Etc/Universal\0" + "Etc/Zulu\0" + "Europe/Amsterdam\0" + "Europe/Andorra\0" + "Europe/Astrakhan\0" + "Europe/Athens\0" + "Europe/Belfast\0" + "Europe/Belgrade\0" + "Europe/Berlin\0" + "Europe/Bratislava\0" + "Europe/Brussels\0" + "Europe/Bucharest\0" + "Europe/Budapest\0" + "Europe/Busingen\0" + "Europe/Chisinau\0" + "Europe/Copenhagen\0" + "Europe/Dublin\0" + "Europe/Gibraltar\0" + "Europe/Guernsey\0" + "Europe/Helsinki\0" + "Europe/Isle_of_Man\0" + "Europe/Istanbul\0" + "Europe/Jersey\0" + "Europe/Kaliningrad\0" + "Europe/Kiev\0" + "Europe/Kirov\0" + "Europe/Kyiv\0" + "Europe/Lisbon\0" + "Europe/Ljubljana\0" + "Europe/London\0" + "Europe/Luxembourg\0" + "Europe/Madrid\0" + "Europe/Malta\0" + "Europe/Mariehamn\0" + "Europe/Minsk\0" + "Europe/Monaco\0" + "Europe/Moscow\0" + "Europe/Nicosia\0" + "Europe/Oslo\0" + "Europe/Paris\0" + "Europe/Podgorica\0" + "Europe/Prague\0" + "Europe/Riga\0" + "Europe/Rome\0" + "Europe/Samara\0" + "Europe/San_Marino\0" + "Europe/Sarajevo\0" + "Europe/Saratov\0" + "Europe/Simferopol\0" + "Europe/Skopje\0" + "Europe/Sofia\0" + "Europe/Stockholm\0" + "Europe/Tallinn\0" + "Europe/Tirane\0" + "Europe/Tiraspol\0" + "Europe/Ulyanovsk\0" + "Europe/Uzhgorod\0" + "Europe/Vaduz\0" + "Europe/Vatican\0" + "Europe/Vienna\0" + "Europe/Vilnius\0" + "Europe/Volgograd\0" + "Europe/Warsaw\0" + "Europe/Zagreb\0" + "Europe/Zaporozhye\0" + "Europe/Zurich\0" + "GB\0" + "GB-Eire\0" + "GMT\0" + "GMT+0\0" + "GMT-0\0" + "GMT0\0" + "Greenwich\0" + "HST\0" + "Hongkong\0" + "Iceland\0" + "Indian/Antananarivo\0" + "Indian/Chagos\0" + "Indian/Christmas\0" + "Indian/Cocos\0" + "Indian/Comoro\0" + "Indian/Kerguelen\0" + "Indian/Mahe\0" + "Indian/Maldives\0" + "Indian/Mauritius\0" + "Indian/Mayotte\0" + "Indian/Reunion\0" + "Iran\0" + "Israel\0" + "Jamaica\0" + "Japan\0" + "Kwajalein\0" + "Libya\0" + "MET\0" + "MST\0" + "MST7MDT\0" + "Mexico/BajaNorte\0" + "Mexico/BajaSur\0" + "Mexico/General\0" + "NZ\0" + "NZ-CHAT\0" + "Navajo\0" + "PRC\0" + "PST8PDT\0" + "Pacific/Apia\0" + "Pacific/Auckland\0" + "Pacific/Bougainville\0" + "Pacific/Chatham\0" + "Pacific/Chuuk\0" + "Pacific/Easter\0" + "Pacific/Efate\0" + "Pacific/Enderbury\0" + "Pacific/Fakaofo\0" + "Pacific/Fiji\0" + "Pacific/Funafuti\0" + "Pacific/Galapagos\0" + "Pacific/Gambier\0" + "Pacific/Guadalcanal\0" + "Pacific/Guam\0" + "Pacific/Honolulu\0" + "Pacific/Johnston\0" + "Pacific/Kanton\0" + "Pacific/Kiritimati\0" + "Pacific/Kosrae\0" + "Pacific/Kwajalein\0" + "Pacific/Majuro\0" + "Pacific/Marquesas\0" + "Pacific/Midway\0" + "Pacific/Nauru\0" + "Pacific/Niue\0" + "Pacific/Norfolk\0" + "Pacific/Noumea\0" + "Pacific/Pago_Pago\0" + "Pacific/Palau\0" + "Pacific/Pitcairn\0" + "Pacific/Pohnpei\0" + "Pacific/Ponape\0" + "Pacific/Port_Moresby\0" + "Pacific/Rarotonga\0" + "Pacific/Saipan\0" + "Pacific/Samoa\0" + "Pacific/Tahiti\0" + "Pacific/Tarawa\0" + "Pacific/Tongatapu\0" + "Pacific/Truk\0" + "Pacific/Wake\0" + "Pacific/Wallis\0" + "Pacific/Yap\0" + "Poland\0" + "Portugal\0" + "ROC\0" + "ROK\0" + "Singapore\0" + "Turkey\0" + "UCT\0" + "US/Alaska\0" + "US/Aleutian\0" + "US/Arizona\0" + "US/Central\0" + "US/East-Indiana\0" + "US/Eastern\0" + "US/Hawaii\0" + "US/Indiana-Starke\0" + "US/Michigan\0" + "US/Mountain\0" + "US/Pacific\0" + "US/Samoa\0" + "UTC\0" + "Universal\0" + "W-SU\0" + "WET\0" + "Zulu\0"; +static const char scr_tz_name_text[] = + "Greenwich Mean Time\0" + "East Africa Time\0" + "Central European Standard Time\0" + "Central European Summer Time\0" + "West Africa Time\0" + "Central Africa Time\0" + "Eastern European Standard Time\0" + "Eastern European Summer Time\0" + "Western European Standard Time\0" + "Western European Summer Time\0" + "South Africa Standard Time\0" + "GMT+02:00\0" + "GMT+01:00\0" + "Hawaii-Aleutian Standard Time\0" + "Hawaii-Aleutian Daylight Time\0" + "Alaska Standard Time\0" + "Alaska Daylight Time\0" + "Atlantic Standard Time\0" + "Brasilia Standard Time\0" + "Brasilia Summer Time\0" + "Argentina Standard Time\0" + "Argentina Summer Time\0" + "Paraguay Standard Time\0" + "Paraguay Summer Time\0" + "Eastern Standard Time\0" + "Central Standard Time\0" + "Central Daylight Time\0" + "Atlantic Daylight Time\0" + "Amazon Standard Time\0" + "Amazon Summer Time\0" + "Colombia Standard Time\0" + "Colombia Summer Time\0" + "Mountain Standard Time\0" + "Mountain Daylight Time\0" + "Eastern Daylight Time\0" + "Venezuela Time\0" + "French Guiana Time\0" + "GMT-03:00\0" + "GMT+00:00\0" + "Yukon Time\0" + "GMT-07:00\0" + "Acre Standard Time\0" + "Acre Summer Time\0" + "Pacific Standard Time\0" + "Pacific Daylight Time\0" + "Greenland Standard Time\0" + "Greenland Summer Time\0" + "Ecuador Time\0" + "GMT-05:00\0" + "Guyana Time\0" + "Cuba Standard Time\0" + "Cuba Daylight Time\0" + "Mexican Pacific Standard Time\0" + "Mexican Pacific Daylight Time\0" + "Bolivia Time\0" + "Peru Standard Time\0" + "Peru Summer Time\0" + "St. Pierre & Miquelon Standard Time\0" + "St. Pierre & Miquelon Daylight Time\0" + "Uruguay Standard Time\0" + "Uruguay Summer Time\0" + "Fernando de Noronha Standard Time\0" + "Fernando de Noronha Summer Time\0" + "Suriname Time\0" + "Chile Standard Time\0" + "Chile Summer Time\0" + "Newfoundland Standard Time\0" + "Newfoundland Daylight Time\0" + "Australian Western Standard Time\0" + "Davis Time\0" + "Dumont d’Urville Time\0" + "Australian Eastern Standard Time\0" + "Australian Eastern Daylight Time\0" + "Mawson Time\0" + "New Zealand Standard Time\0" + "New Zealand Daylight Time\0" + "Rothera Time\0" + "Syowa Time\0" + "Vostok Time\0" + "Arabian Standard Time\0" + "Kazakhstan Time\0" + "GMT+05:00\0" + "GMT+03:00\0" + "Kamchatka Standard Time\0" + "Kamchatka Summer Time\0" + "Turkmenistan Standard Time\0" + "Turkmenistan Summer Time\0" + "Arabian Daylight Time\0" + "Azerbaijan Standard Time\0" + "Azerbaijan Summer Time\0" + "Indochina Time\0" + "Krasnoyarsk Standard Time\0" + "Krasnoyarsk Summer Time\0" + "Kyrgyzstan Time\0" + "GMT+06:00\0" + "Brunei Time\0" + "India Standard Time\0" + "Yakutsk Standard Time\0" + "Yakutsk Summer Time\0" + "Ulaanbaatar Standard Time\0" + "Ulaanbaatar Summer Time\0" + "China Standard Time\0" + "China Daylight Time\0" + "Bangladesh Standard Time\0" + "Bangladesh Summer Time\0" + "Timor-Leste Time\0" + "Gulf Standard Time\0" + "Tajikistan Time\0" + "Hong Kong Standard Time\0" + "Hong Kong Summer Time\0" + "Khovd Standard Time\0" + "Khovd Summer Time\0" + "Irkutsk Standard Time\0" + "Irkutsk Summer Time\0" + "Türkiye Standard Time\0" + "Türkiye Summer Time\0" + "Western Indonesia Time\0" + "Eastern Indonesia Time\0" + "Israel Standard Time\0" + "Israel Daylight Time\0" + "Afghanistan Time\0" + "Pakistan Standard Time\0" + "Pakistan Summer Time\0" + "Nepal Time\0" + "Malaysia Time\0" + "Magadan Standard Time\0" + "Magadan Summer Time\0" + "Central Indonesia Time\0" + "Philippine Standard Time\0" + "Philippine Summer Time\0" + "Omsk Standard Time\0" + "Omsk Summer Time\0" + "Korean Standard Time\0" + "Myanmar Time\0" + "Uzbekistan Standard Time\0" + "Uzbekistan Summer Time\0" + "Korean Daylight Time\0" + "Singapore Standard Time\0" + "Taiwan Standard Time\0" + "Taiwan Daylight Time\0" + "Georgia Standard Time\0" + "Georgia Summer Time\0" + "Iran Standard Time\0" + "Iran Daylight Time\0" + "Bhutan Time\0" + "Japan Standard Time\0" + "Vladivostok Standard Time\0" + "Vladivostok Summer Time\0" + "Yekaterinburg Standard Time\0" + "Yekaterinburg Summer Time\0" + "Armenia Standard Time\0" + "Armenia Summer Time\0" + "Azores Standard Time\0" + "Azores Summer Time\0" + "Cape Verde Standard Time\0" + "South Georgia Time\0" + "Falkland Islands Standard Time\0" + "Falkland Islands Summer Time\0" + "Australian Central Standard Time\0" + "Australian Central Daylight Time\0" + "Australian Central Western Standard Time\0" + "Australian Central Western Daylight Time\0" + "Lord Howe Standard Time\0" + "Lord Howe Daylight Time\0" + "Australian Western Daylight Time\0" + "Easter Island Standard Time\0" + "Easter Island Summer Time\0" + "GMT-01:00\0" + "GMT-10:00\0" + "GMT-11:00\0" + "GMT-12:00\0" + "GMT-02:00\0" + "GMT-04:00\0" + "GMT-06:00\0" + "GMT-08:00\0" + "GMT-09:00\0" + "GMT+10:00\0" + "GMT+11:00\0" + "GMT+12:00\0" + "GMT+13:00\0" + "GMT+14:00\0" + "GMT+04:00\0" + "GMT+07:00\0" + "GMT+08:00\0" + "GMT+09:00\0" + "Coordinated Universal Time\0" + "Samara Standard Time\0" + "Samara Summer Time\0" + "British Summer Time\0" + "Irish Standard Time\0" + "Moscow Standard Time\0" + "Moscow Summer Time\0" + "Indian Ocean Time\0" + "Christmas Island Time\0" + "Cocos Islands Time\0" + "French Southern & Antarctic Time\0" + "Seychelles Time\0" + "Maldives Time\0" + "Mauritius Standard Time\0" + "Mauritius Summer Time\0" + "Réunion Time\0" + "Marshall Islands Time\0" + "Chatham Standard Time\0" + "Chatham Daylight Time\0" + "Samoa Standard Time\0" + "Samoa Daylight Time\0" + "Chuuk Time\0" + "Vanuatu Standard Time\0" + "Vanuatu Summer Time\0" + "Phoenix Islands Time\0" + "Tokelau Time\0" + "Fiji Standard Time\0" + "Fiji Summer Time\0" + "Tuvalu Time\0" + "Galapagos Time\0" + "Gambier Time\0" + "Solomon Islands Time\0" + "Chamorro Standard Time\0" + "Line Islands Time\0" + "Kosrae Time\0" + "Marquesas Time\0" + "American Samoa Standard Time\0" + "Nauru Time\0" + "Niue Time\0" + "Norfolk Island Standard Time\0" + "Norfolk Island Daylight Time\0" + "New Caledonia Standard Time\0" + "New Caledonia Summer Time\0" + "Palau Time\0" + "Pitcairn Time\0" + "Pohnpei Time\0" + "Papua New Guinea Time\0" + "Cook Islands Standard Time\0" + "Cook Islands Summer Time\0" + "Tahiti Time\0" + "Gilbert Islands Time\0" + "Tonga Standard Time\0" + "Tonga Summer Time\0" + "Wake Island Time\0" + "Wallis & Futuna Time\0"; +static const int32_t scr_tz_daylight_changes[] = { + 141264000, 147222000, 199756800, 207702000, 231292800, 244249200, 265507200, 271033200, + 1212278400, 1220223600, 1243814400, 1250809200, 1272758400, 1281222000, 1301788800, 1312066800, + 1335664800, 1342749600, 1345428000, 1348970400, 1367114400, 1373162400, 1376100000, 1382839200, + 1396144800, 1403920800, 1406944800, 1414288800, 1427594400, 1434247200, 1437271200, 1445738400, + 1459044000, 1465092000, 1468116000, 1477792800, 1490493600, 1495332000, 1498960800, 1509242400, + 1521943200, 1526176800, 1529200800, 1557021600, 1560045600, 1587261600, 1590890400, 1618106400, + 1621130400, 1648346400, 1651975200, 1679191200, 1682215200, 1710036000, 1713060000, 1740276000, + 1743904800, 1771120800, 1774144800, 1789866000, 199756800, 207702000, 231292800, 244249200, + 265507200, 271033200, 1212278400, 1220223600, 1243814400, 1250809200, 1272758400, 1281222000, + 1301788800, 1312066800, 1335664800, 1342749600, 1345428000, 1348970400, 1367114400, 1373162400, + 1376100000, 1382839200, 1396144800, 1403920800, 1406944800, 1414288800, 1427594400, 1434247200, + 1437271200, 1445738400, 1459044000, 1465092000, 1468116000, 1477792800, 1490493600, 1495332000, + 1498960800, 1509242400, 1521943200, 1526176800, 1529200800, 1557021600, 1560045600, 1587261600, + 1590890400, 1618106400, 1621130400, 1648346400, 1651975200, 1679191200, 1682215200, 1710036000, + 1713060000, 1740276000, 1743904800, 1771120800, 1774144800, 1789866000, 778640400, 796780800, + 810090000, 828835200, 841539600, 860284800, 873594000, 891734400, 905043600, 923184000, 936493200, + 954633600, 967942800, 986083200, 999392400, 1018137600, 1030842000, 1049587200, 1062896400, + 1081036800, 1094346000, 1112486400, 1125795600, 1143936000, 1157245200, 1175385600, 1188694800, + 1207440000, 1220749200, 1238889600, 1252198800, 1270339200, 1283648400, 1301788800, 1315098000, + 1333238400, 1346547600, 1365292800, 1377997200, 1396742400, 1410051600, 1428192000, 1441501200, + 1459641600, 1472950800, 1491091200, 1504400400, 1508796000, 73472400, 89193600, 104922000, + 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, 247046400, + 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, 388573200, + 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, 530697600, + 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, 671014800, + 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, 814953600, + 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, 954666000, + 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, 1081069200, + 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, 1205053200, + 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, 1331456400, + 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, 1457859600, + 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, 1583658000, + 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, 1710061200, + 1730620800, 1741510800, 1762070400, 1772960400, 9972000, 25693200, 41421600, 57747600, 73476000, + 89197200, 104925600, 120646800, 136375200, 152096400, 167824800, 183546000, 199274400, 215600400, + 230724000, 247050000, 262778400, 278499600, 294228000, 309949200, 325677600, 341398800, 357127200, + 372848400, 388576800, 404902800, 420026400, 436352400, 452080800, 467802000, 483530400, 499251600, + 514980000, 530701200, 544615200, 562150800, 576064800, 594205200, 607514400, 625654800, 638964000, + 657104400, 671018400, 688554000, 702468000, 720003600, 733917600, 752058000, 765367200, 783507600, + 796816800, 814957200, 828871200, 846406800, 860320800, 877856400, 891770400, 909306000, 923220000, + 941360400, 954669600, 972810000, 986119200, 1004259600, 1018173600, 1035709200, 1049623200, + 1067158800, 1081072800, 1099213200, 1112522400, 1130662800, 1143972000, 1162112400, 1173607200, + 1194166800, 1205056800, 1225616400, 1236506400, 1257066000, 1268560800, 1289120400, 1300010400, + 1320570000, 1331460000, 1352019600, 1362909600, 1383469200, 1394359200, 1414918800, 1425808800, + 1446368400, 1457863200, 1478422800, 1489312800, 1509872400, 1520762400, 1541322000, 1552212000, + 1572771600, 1583661600, 1604221200, 1615716000, 1636275600, 1647165600, 1667725200, 1678615200, + 1699174800, 1710064800, 1730624400, 1741514400, 1762074000, 1772964000, 73472400, 89193600, + 104922000, 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, + 247046400, 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, + 388573200, 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, + 530697600, 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, + 671014800, 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, + 814953600, 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, + 954666000, 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, + 1081069200, 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, + 1205053200, 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, + 1331456400, 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, + 1457859600, 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, + 1583658000, 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, + 1710061200, 1730620800, 1741510800, 1762070400, 1772960400, 73472400, 89193600, 104922000, + 120643200, 136371600, 152092800, 167821200, 183542400, 199270800, 215596800, 230720400, 247046400, + 262774800, 278496000, 294224400, 309945600, 325674000, 341395200, 357123600, 372844800, 388573200, + 404899200, 420022800, 436348800, 452077200, 467798400, 483526800, 499248000, 514976400, 530697600, + 544611600, 562147200, 576061200, 594201600, 607510800, 625651200, 638960400, 657100800, 671014800, + 688550400, 702464400, 720000000, 733914000, 752054400, 765363600, 783504000, 796813200, 814953600, + 828867600, 846403200, 860317200, 877852800, 891766800, 909302400, 923216400, 941356800, 954666000, + 972806400, 986115600, 1004256000, 1018170000, 1035705600, 1049619600, 1067155200, 1081069200, + 1099209600, 1112518800, 1130659200, 1143968400, 1162108800, 1173603600, 1194163200, 1205053200, + 1225612800, 1236502800, 1257062400, 1268557200, 1289116800, 1300006800, 1320566400, 1331456400, + 1352016000, 1362906000, 1383465600, 1394355600, 1414915200, 1425805200, 1446364800, 1457859600, + 1478419200, 1489309200, 1509868800, 1520758800, 1541318400, 1552208400, 1572768000, 1583658000, + 1604217600, 1615712400, 1636272000, 1647162000, 1667721600, 1678611600, 1699171200, 1710061200, + 1730620800, 1741510800, 1762070400, 1772960400, 9972000, 25693200, 41421600, 57747600, 73476000, + 89197200, 104925600, 120646800, 136375200, 152096400, 167824800, 183546000, 199274400, 215600400, + 230724000, 247050000, 262778400, 278499600, 294228000, 309949200, 325677600, 341398800, 357127200, + 372848400, 388576800, 404902800, 420026400, 436352400, 452080800, 467802000, 483530400, 499251600, + 514980000, 530701200, 544615200, 562150800, 576064800, 594205200, 607514400, 625654800, 638964000, + 657104400, 671018400, 688554000, 702468000, 720003600, 733917600, 752058000, 765367200, 783507600, + 796816800, 814957200, 828871200, 846406800, 860320800, 877856400, 891770400, 909306000, 923220000, + 941360400, 954669600, 972810000, 986119200, 1004259600, 1018173600, 1035709200, 1049623200, + 1067158800, 1081072800, 1099213200, 1112522400, 1130662800, 1143972000, 1162112400, 1173607200, + 1194166800, 1205056800, 1225616400, 1236506400, 1257066000, 1268560800, 1289120400, 1300010400, + 1320570000, 1331460000, 1352019600, 1362909600, 1383469200, 1394359200, 1414918800, 1425808800, + 1446368400, 1457863200, 1478422800, 1489312800, 1509872400, 1520762400, 1541322000, 1552212000, + 1572771600, 1583661600, 1604221200, 1615716000, 1636275600, 1647165600, 1667725200, 1678615200, + 1699174800, 1710064800, 1730624400, 1741514400, 1762074000, 1772964000, 69818400, 89172000, + 101268000, 120621600, 132717600, 152071200, 164167200, 183520800, 196221600, 214970400, 227671200, + 246420000, 259120800, 278474400, 290570400, 309924000, 322020000, 341373600, 354675600, 372819600, + 386125200, 404269200, 417574800, 435718800, 449024400, 467773200, 481078800, 499222800, 512528400, + 530672400, 543978000, 562122000, 575427600, 593571600, 606877200, 625626000, 638326800, 657075600, + 670381200, 688525200, 701830800, 719974800, 733280400, 751424400, 764730000, 782874000, 796179600, + 814323600, 828234000, 846378000, 859683600, 877827600, 891133200, 909277200, 922582800, 941331600, + 954032400, 972781200, 985482000, 1004230800, 1017536400, 1035680400, 1048986000, 1067130000, + 1080435600, 1099184400, 1111885200, 1130634000, 1143334800, 1162083600, 1174784400, 1193533200, + 1206838800, 1224982800, 1238288400, 1256432400, 1269738000, 1288486800, 1301187600, 1319936400, + 1332637200, 1351386000, 1364691600, 1382835600, 1396141200, 1414285200, 1427590800, 1445734800, + 1459040400, 1477789200, 1490490000, 1509238800, 1521939600, 1540688400, 1553994000, 1572138000, + 1585443600, 1603587600, 1616893200, 1635642000, 1648342800, 1667091600, 1679792400, 1698541200, + 1711846800, 1729990800, 1743296400, 1761440400, 1774746000, 1792890000, 1806195600, 1824944400, + 1837645200, 1856394000, 1869094800, 1887843600, 1901149200, 1919293200, 1932598800, 1950742800, + 1964048400, 1982797200, 1995498000, 2014246800, 2026947600, 2045696400, 2058397200, 2077146000, + 2090451600, 2108595600, 2121901200, 2140045200, +}; +static const ScrTzNames scr_tz_names[] = { + { 0, 0, 0, 0, 0 }, + { 15, 0, 0, 0, 0 }, + { 28, 20, 20, 0, 0 }, + { 47, 37, 68, 0, 0 }, + { 62, 20, 20, 0, 0 }, + { 76, 20, 20, 0, 0 }, + { 90, 0, 0, 0, 0 }, + { 104, 97, 97, 0, 0 }, + { 118, 0, 0, 0, 0 }, + { 132, 0, 0, 0, 0 }, + { 146, 114, 114, 0, 0 }, + { 162, 97, 97, 0, 0 }, + { 181, 114, 114, 0, 0 }, + { 198, 134, 165, 0, 0 }, + { 211, 194, 225, 0, 60 }, + { 229, 37, 68, 0, 0 }, + { 242, 0, 0, 0, 0 }, + { 257, 0, 0, 0, 0 }, + { 270, 20, 20, 0, 0 }, + { 291, 20, 20, 0, 0 }, + { 307, 97, 97, 0, 0 }, + { 321, 194, 225, 60, 58 }, + { 337, 0, 0, 0, 0 }, + { 353, 114, 114, 0, 0 }, + { 369, 114, 114, 0, 0 }, + { 383, 254, 254, 0, 0 }, + { 403, 114, 281, 0, 0 }, + { 415, 20, 20, 0, 0 }, + { 430, 114, 281, 0, 0 }, + { 446, 114, 114, 0, 0 }, + { 460, 97, 97, 0, 0 }, + { 476, 97, 97, 0, 0 }, + { 489, 97, 97, 0, 0 }, + { 507, 0, 0, 0, 0 }, + { 519, 97, 97, 0, 0 }, + { 533, 114, 114, 0, 0 }, + { 551, 114, 114, 0, 0 }, + { 565, 97, 97, 0, 0 }, + { 579, 114, 114, 0, 0 }, + { 593, 254, 254, 0, 0 }, + { 607, 254, 254, 0, 0 }, + { 622, 20, 20, 0, 0 }, + { 639, 0, 0, 0, 0 }, + { 655, 20, 20, 0, 0 }, + { 670, 97, 291, 0, 0 }, + { 686, 97, 97, 0, 0 }, + { 700, 0, 0, 0, 0 }, + { 718, 0, 0, 0, 0 }, + { 737, 97, 97, 0, 0 }, + { 755, 0, 0, 0, 0 }, + { 771, 0, 0, 0, 0 }, + { 787, 134, 165, 0, 0 }, + { 802, 37, 68, 0, 0 }, + { 815, 114, 281, 118, 48 }, + { 831, 301, 331, 0, 0 }, + { 844, 361, 382, 0, 0 }, + { 862, 403, 403, 0, 0 }, + { 879, 403, 403, 0, 0 }, + { 895, 426, 449, 0, 0 }, + { 913, 470, 494, 0, 0 }, + { 944, 470, 494, 0, 0 }, + { 972, 470, 494, 0, 0 }, + { 1005, 470, 494, 0, 0 }, + { 1031, 470, 494, 0, 0 }, + { 1055, 470, 494, 0, 0 }, + { 1082, 470, 494, 0, 0 }, + { 1108, 470, 494, 0, 0 }, + { 1139, 470, 494, 0, 0 }, + { 1163, 470, 494, 0, 0 }, + { 1190, 470, 494, 0, 0 }, + { 1217, 470, 494, 0, 0 }, + { 1243, 470, 494, 0, 0 }, + { 1269, 403, 403, 0, 0 }, + { 1283, 516, 539, 0, 0 }, + { 1300, 560, 560, 0, 0 }, + { 1317, 301, 331, 0, 0 }, + { 1330, 426, 449, 0, 0 }, + { 1344, 582, 604, 0, 0 }, + { 1367, 403, 626, 0, 0 }, + { 1384, 426, 449, 0, 0 }, + { 1398, 582, 604, 0, 0 }, + { 1413, 403, 403, 0, 0 }, + { 1434, 649, 670, 0, 0 }, + { 1452, 689, 712, 0, 0 }, + { 1467, 733, 756, 0, 0 }, + { 1481, 470, 494, 0, 0 }, + { 1502, 733, 756, 0, 0 }, + { 1524, 649, 670, 0, 0 }, + { 1545, 560, 779, 0, 0 }, + { 1560, 801, 801, 0, 0 }, + { 1576, 470, 494, 0, 0 }, + { 1594, 816, 816, 0, 0 }, + { 1610, 560, 560, 0, 0 }, + { 1625, 582, 604, 0, 0 }, + { 1641, 582, 604, 0, 0 }, + { 1659, 733, 756, 0, 0 }, + { 1681, 560, 560, 0, 0 }, + { 1703, 470, 494, 0, 0 }, + { 1719, 582, 604, 0, 0 }, + { 1738, 835, 835, 0, 0 }, + { 1756, 733, 733, 0, 0 }, + { 1772, 649, 670, 0, 0 }, + { 1787, 403, 403, 0, 0 }, + { 1803, 0, 845, 0, 0 }, + { 1824, 855, 866, 0, 0 }, + { 1839, 733, 756, 0, 0 }, + { 1860, 733, 756, 0, 0 }, + { 1875, 560, 779, 0, 0 }, + { 1891, 403, 403, 0, 0 }, + { 1908, 733, 756, 166, 109 }, + { 1925, 876, 895, 0, 0 }, + { 1942, 582, 604, 0, 0 }, + { 1962, 912, 934, 0, 0 }, + { 1979, 733, 756, 0, 0 }, + { 1999, 560, 779, 0, 0 }, + { 2018, 426, 449, 0, 0 }, + { 2036, 403, 626, 0, 0 }, + { 2054, 956, 980, 0, 0 }, + { 2070, 403, 626, 0, 0 }, + { 2088, 560, 779, 0, 0 }, + { 2107, 403, 403, 0, 0 }, + { 2123, 403, 403, 0, 0 }, + { 2142, 582, 604, 0, 0 }, + { 2160, 1002, 1015, 0, 0 }, + { 2178, 1025, 1025, 0, 0 }, + { 2193, 403, 626, 0, 0 }, + { 2209, 1037, 1056, 0, 0 }, + { 2224, 1075, 1105, 0, 0 }, + { 2243, 560, 779, 0, 0 }, + { 2272, 582, 604, 0, 0 }, + { 2293, 560, 779, 0, 0 }, + { 2317, 560, 779, 0, 0 }, + { 2344, 582, 604, 0, 0 }, + { 2370, 560, 779, 0, 0 }, + { 2392, 560, 779, 0, 0 }, + { 2418, 560, 779, 0, 0 }, + { 2442, 560, 779, 0, 0 }, + { 2463, 733, 756, 0, 0 }, + { 2478, 560, 779, 0, 0 }, + { 2494, 560, 779, 0, 0 }, + { 2510, 470, 494, 0, 0 }, + { 2524, 361, 382, 0, 0 }, + { 2539, 560, 779, 0, 0 }, + { 2567, 560, 779, 0, 0 }, + { 2595, 582, 604, 0, 0 }, + { 2611, 403, 403, 0, 0 }, + { 2630, 1135, 1135, 0, 0 }, + { 2645, 1148, 1167, 0, 0 }, + { 2658, 912, 934, 0, 0 }, + { 2678, 560, 779, 0, 0 }, + { 2697, 403, 403, 0, 0 }, + { 2719, 426, 449, 0, 0 }, + { 2734, 582, 604, 0, 0 }, + { 2750, 649, 670, 0, 0 }, + { 2765, 403, 403, 0, 0 }, + { 2781, 403, 626, 0, 0 }, + { 2800, 582, 604, 0, 0 }, + { 2818, 1075, 1105, 0, 0 }, + { 2835, 470, 494, 0, 0 }, + { 2851, 582, 604, 0, 0 }, + { 2869, 582, 604, 0, 0 }, + { 2884, 361, 382, 0, 0 }, + { 2903, 582, 604, 0, 0 }, + { 2923, 1184, 1220, 0, 0 }, + { 2940, 403, 626, 0, 0 }, + { 2956, 582, 604, 0, 0 }, + { 2974, 1256, 1278, 0, 0 }, + { 2993, 560, 779, 0, 0 }, + { 3010, 403, 403, 0, 0 }, + { 3029, 560, 779, 0, 0 }, + { 3044, 560, 779, 0, 0 }, + { 3061, 560, 779, 0, 0 }, + { 3077, 361, 382, 0, 0 }, + { 3090, 1298, 1332, 0, 0 }, + { 3106, 582, 604, 0, 0 }, + { 3134, 582, 604, 0, 0 }, + { 3162, 582, 604, 0, 0 }, + { 3193, 956, 980, 0, 0 }, + { 3206, 582, 604, 0, 0 }, + { 3222, 560, 560, 0, 0 }, + { 3237, 560, 779, 0, 0 }, + { 3257, 1364, 1364, 0, 0 }, + { 3276, 733, 733, 0, 0 }, + { 3292, 560, 779, 0, 0 }, + { 3315, 403, 403, 0, 0 }, + { 3337, 876, 895, 0, 0 }, + { 3356, 649, 670, 0, 0 }, + { 3376, 403, 403, 0, 0 }, + { 3396, 835, 835, 0, 0 }, + { 3417, 582, 604, 0, 0 }, + { 3437, 582, 604, 0, 0 }, + { 3458, 426, 449, 0, 0 }, + { 3473, 582, 582, 0, 0 }, + { 3488, 582, 604, 0, 0 }, + { 3505, 876, 895, 0, 0 }, + { 3524, 470, 494, 0, 0 }, + { 3540, 912, 934, 0, 0 }, + { 3561, 426, 449, 0, 0 }, + { 3578, 1378, 1398, 0, 0 }, + { 3595, 403, 626, 0, 0 }, + { 3617, 426, 449, 0, 0 }, + { 3635, 956, 980, 0, 0 }, + { 3656, 733, 756, 0, 0 }, + { 3673, 361, 382, 0, 0 }, + { 3687, 403, 403, 0, 0 }, + { 3709, 1416, 1443, 0, 0 }, + { 3726, 403, 403, 0, 0 }, + { 3743, 403, 403, 0, 0 }, + { 3760, 403, 403, 0, 0 }, + { 3778, 403, 403, 0, 0 }, + { 3797, 582, 582, 0, 0 }, + { 3819, 582, 604, 0, 0 }, + { 3839, 403, 626, 0, 0 }, + { 3853, 560, 779, 0, 0 }, + { 3873, 912, 934, 0, 0 }, + { 3889, 560, 779, 0, 0 }, + { 3905, 403, 403, 0, 0 }, + { 3921, 912, 934, 275, 113 }, + { 3939, 403, 403, 0, 0 }, + { 3954, 855, 866, 0, 0 }, + { 3973, 582, 604, 0, 0 }, + { 3990, 361, 382, 0, 0 }, + { 4006, 733, 756, 388, 109 }, + { 4026, 1470, 1470, 0, 0 }, + { 4043, 1503, 1503, 0, 0 }, + { 4060, 1514, 1514, 0, 0 }, + { 4086, 1538, 1571, 0, 0 }, + { 4107, 1604, 1604, 0, 0 }, + { 4125, 1616, 1642, 0, 0 }, + { 4144, 835, 835, 0, 0 }, + { 4162, 1668, 1668, 0, 0 }, + { 4181, 1616, 1642, 0, 0 }, + { 4203, 1681, 1681, 0, 0 }, + { 4220, 0, 281, 0, 0 }, + { 4237, 1692, 1692, 0, 0 }, + { 4255, 37, 68, 0, 0 }, + { 4275, 1704, 1704, 0, 0 }, + { 4285, 1726, 1742, 0, 0 }, + { 4297, 1752, 1752, 0, 0 }, + { 4308, 1762, 1786, 0, 0 }, + { 4320, 1726, 1742, 0, 0 }, + { 4331, 1726, 1742, 0, 0 }, + { 4343, 1808, 1835, 0, 0 }, + { 4357, 1808, 1835, 0, 0 }, + { 4372, 1726, 1742, 0, 0 }, + { 4384, 1704, 1860, 0, 0 }, + { 4397, 1704, 1704, 0, 0 }, + { 4410, 1882, 1907, 0, 0 }, + { 4420, 1930, 1930, 0, 0 }, + { 4433, 1945, 1971, 0, 0 }, + { 4446, 134, 165, 0, 0 }, + { 4458, 1995, 2011, 0, 0 }, + { 4471, 2021, 2021, 0, 0 }, + { 4483, 2033, 2033, 0, 0 }, + { 4497, 2053, 2075, 0, 0 }, + { 4508, 2095, 2121, 0, 0 }, + { 4524, 2145, 2165, 0, 0 }, + { 4539, 2145, 2165, 0, 0 }, + { 4554, 2033, 2033, 0, 0 }, + { 4567, 2185, 2210, 0, 0 }, + { 4578, 1752, 1752, 0, 0 }, + { 4592, 2185, 2210, 0, 0 }, + { 4603, 2233, 2233, 0, 0 }, + { 4613, 2250, 2250, 0, 0 }, + { 4624, 2269, 1742, 0, 0 }, + { 4638, 134, 165, 0, 0 }, + { 4653, 134, 165, 0, 0 }, + { 4663, 2145, 2165, 0, 0 }, + { 4675, 134, 165, 0, 0 }, + { 4687, 1930, 1930, 0, 0 }, + { 4704, 2285, 2309, 0, 0 }, + { 4719, 2331, 2351, 0, 0 }, + { 4729, 2369, 2391, 0, 0 }, + { 4742, 2411, 2434, 0, 0 }, + { 4756, 2455, 2455, 0, 0 }, + { 4769, 2478, 2478, 0, 0 }, + { 4783, 2501, 2522, 0, 0 }, + { 4798, 2543, 2543, 0, 0 }, + { 4809, 1762, 1786, 0, 0 }, + { 4824, 2560, 2583, 0, 0 }, + { 4837, 2011, 2011, 0, 0 }, + { 4850, 2604, 2604, 0, 0 }, + { 4865, 2604, 2604, 0, 0 }, + { 4879, 2053, 2075, 0, 0 }, + { 4893, 2033, 2033, 0, 0 }, + { 4906, 1945, 1971, 0, 0 }, + { 4923, 2615, 2615, 0, 0 }, + { 4941, 2615, 2615, 0, 0 }, + { 4954, 1704, 1704, 0, 0 }, + { 4966, 2145, 2165, 0, 0 }, + { 4977, 2145, 2165, 0, 0 }, + { 4988, 2629, 2651, 0, 0 }, + { 5001, 2671, 2671, 0, 0 }, + { 5015, 2694, 2719, 0, 0 }, + { 5027, 2250, 2250, 0, 0 }, + { 5039, 134, 165, 0, 0 }, + { 5052, 1945, 1971, 0, 0 }, + { 5070, 1945, 1971, 0, 0 }, + { 5087, 2742, 2761, 0, 0 }, + { 5097, 1726, 1742, 0, 0 }, + { 5107, 1930, 1930, 0, 0 }, + { 5123, 2455, 2455, 0, 0 }, + { 5138, 2778, 2778, 0, 0 }, + { 5153, 1704, 1704, 0, 0 }, + { 5164, 1726, 1742, 0, 0 }, + { 5178, 1726, 1742, 0, 0 }, + { 5193, 2799, 2799, 0, 0 }, + { 5206, 1704, 1704, 0, 0 }, + { 5218, 1930, 1930, 0, 0 }, + { 5230, 2629, 2651, 0, 0 }, + { 5244, 2812, 2837, 0, 0 }, + { 5259, 2778, 2860, 0, 0 }, + { 5270, 2145, 2165, 0, 0 }, + { 5284, 2881, 2881, 0, 0 }, + { 5299, 2629, 2651, 0, 0 }, + { 5318, 2905, 2926, 0, 0 }, + { 5330, 2812, 2837, 0, 0 }, + { 5344, 2947, 2969, 0, 0 }, + { 5357, 2989, 3008, 0, 0 }, + { 5369, 2501, 2522, 0, 0 }, + { 5383, 3027, 3027, 0, 0 }, + { 5395, 3027, 3027, 0, 0 }, + { 5408, 3039, 3039, 0, 0 }, + { 5419, 1945, 1971, 0, 0 }, + { 5430, 2671, 2671, 0, 0 }, + { 5449, 2095, 2121, 0, 0 }, + { 5466, 2095, 2121, 0, 0 }, + { 5482, 2011, 2011, 0, 0 }, + { 5494, 3059, 3085, 0, 0 }, + { 5508, 1930, 1930, 0, 0 }, + { 5523, 3059, 3085, 0, 0 }, + { 5540, 2053, 2075, 0, 0 }, + { 5553, 2799, 2799, 0, 0 }, + { 5565, 3109, 3137, 0, 0 }, + { 5584, 3163, 3185, 0, 0 }, + { 5597, 3205, 3226, 0, 0 }, + { 5613, 403, 626, 0, 0 }, + { 5630, 194, 225, 0, 0 }, + { 5646, 3245, 3245, 0, 0 }, + { 5666, 194, 225, 0, 0 }, + { 5682, 194, 225, 0, 0 }, + { 5697, 37, 68, 0, 0 }, + { 5716, 194, 225, 0, 0 }, + { 5733, 0, 0, 0, 0 }, + { 5752, 3270, 3270, 0, 0 }, + { 5775, 0, 0, 0, 0 }, + { 5794, 3289, 3320, 0, 0 }, + { 5811, 1538, 1571, 0, 0 }, + { 5825, 3349, 3382, 0, 0 }, + { 5844, 1538, 1571, 0, 0 }, + { 5863, 3349, 3382, 0, 0 }, + { 5885, 1538, 1571, 0, 0 }, + { 5904, 1538, 1571, 0, 0 }, + { 5921, 3349, 3349, 0, 0 }, + { 5938, 3415, 3456, 0, 0 }, + { 5954, 1538, 1571, 0, 0 }, + { 5971, 3497, 3521, 0, 0 }, + { 5985, 1538, 1571, 0, 0 }, + { 6004, 3497, 3521, 0, 0 }, + { 6024, 1538, 1571, 0, 0 }, + { 6044, 1538, 1571, 0, 0 }, + { 6058, 3349, 3349, 0, 0 }, + { 6074, 1470, 3545, 0, 0 }, + { 6090, 1538, 1571, 0, 0 }, + { 6111, 3349, 3382, 0, 0 }, + { 6127, 1538, 1571, 0, 0 }, + { 6144, 1538, 1571, 0, 0 }, + { 6163, 1538, 1571, 0, 0 }, + { 6182, 1470, 3545, 0, 0 }, + { 6197, 3349, 3382, 0, 0 }, + { 6218, 876, 895, 0, 0 }, + { 6230, 1298, 1332, 0, 0 }, + { 6247, 426, 449, 0, 0 }, + { 6259, 649, 670, 0, 0 }, + { 6271, 37, 68, 0, 0 }, + { 6275, 582, 604, 0, 0 }, + { 6283, 403, 626, 0, 0 }, + { 6299, 582, 604, 0, 0 }, + { 6314, 560, 779, 0, 0 }, + { 6329, 733, 756, 497, 109 }, + { 6345, 1416, 1443, 0, 0 }, + { 6365, 912, 934, 606, 113 }, + { 6380, 582, 582, 0, 0 }, + { 6400, 855, 866, 0, 0 }, + { 6413, 1378, 1398, 0, 0 }, + { 6431, 3578, 3606, 0, 0 }, + { 6450, 1037, 1056, 0, 0 }, + { 6455, 134, 165, 0, 0 }, + { 6459, 560, 560, 0, 0 }, + { 6463, 560, 779, 0, 0 }, + { 6471, 134, 165, 0, 0 }, + { 6477, 0, 0, 0, 0 }, + { 6482, 0, 0, 0, 0 }, + { 6490, 0, 0, 0, 0 }, + { 6500, 3632, 3632, 0, 0 }, + { 6510, 3642, 3642, 0, 0 }, + { 6521, 3652, 3652, 0, 0 }, + { 6532, 3662, 3662, 0, 0 }, + { 6543, 3672, 3672, 0, 0 }, + { 6553, 835, 835, 0, 0 }, + { 6563, 3682, 3682, 0, 0 }, + { 6573, 1015, 1015, 0, 0 }, + { 6583, 3692, 3692, 0, 0 }, + { 6593, 866, 866, 0, 0 }, + { 6603, 3702, 3702, 0, 0 }, + { 6613, 3712, 3712, 0, 0 }, + { 6623, 0, 0, 0, 0 }, + { 6633, 291, 291, 0, 0 }, + { 6643, 3722, 3722, 0, 0 }, + { 6654, 3732, 3732, 0, 0 }, + { 6665, 3742, 3742, 0, 0 }, + { 6676, 3752, 3752, 0, 0 }, + { 6687, 3762, 3762, 0, 0 }, + { 6698, 281, 281, 0, 0 }, + { 6708, 1752, 1752, 0, 0 }, + { 6718, 3772, 3772, 0, 0 }, + { 6728, 1742, 1742, 0, 0 }, + { 6738, 2011, 2011, 0, 0 }, + { 6748, 3782, 3782, 0, 0 }, + { 6758, 3792, 3792, 0, 0 }, + { 6768, 3802, 3802, 0, 0 }, + { 6778, 0, 0, 0, 0 }, + { 6787, 0, 0, 0, 0 }, + { 6801, 3812, 3812, 0, 0 }, + { 6809, 3812, 3812, 0, 0 }, + { 6817, 3812, 3812, 0, 0 }, + { 6831, 3812, 3812, 0, 0 }, + { 6840, 37, 68, 0, 0 }, + { 6857, 37, 68, 0, 0 }, + { 6872, 3839, 3860, 0, 0 }, + { 6889, 134, 165, 0, 0 }, + { 6903, 0, 3879, 0, 0 }, + { 6918, 37, 68, 0, 0 }, + { 6934, 37, 68, 0, 0 }, + { 6948, 37, 68, 0, 0 }, + { 6966, 37, 68, 0, 0 }, + { 6982, 134, 165, 0, 0 }, + { 6999, 37, 68, 0, 0 }, + { 7015, 37, 68, 0, 0 }, + { 7031, 134, 165, 0, 0 }, + { 7047, 37, 68, 0, 0 }, + { 7065, 0, 3899, 719, 132 }, + { 7079, 37, 68, 0, 0 }, + { 7096, 0, 291, 0, 0 }, + { 7112, 134, 165, 0, 0 }, + { 7128, 0, 291, 0, 0 }, + { 7147, 2411, 2434, 0, 0 }, + { 7163, 0, 291, 0, 0 }, + { 7177, 134, 165, 0, 0 }, + { 7196, 134, 165, 0, 0 }, + { 7208, 3919, 3940, 0, 0 }, + { 7221, 134, 165, 0, 0 }, + { 7233, 194, 225, 0, 0 }, + { 7247, 37, 68, 0, 0 }, + { 7264, 0, 3879, 0, 0 }, + { 7278, 37, 68, 0, 0 }, + { 7296, 37, 68, 0, 0 }, + { 7310, 37, 68, 0, 0 }, + { 7323, 134, 165, 0, 0 }, + { 7340, 3919, 3940, 0, 0 }, + { 7353, 37, 68, 0, 0 }, + { 7367, 3919, 3940, 0, 0 }, + { 7381, 134, 165, 0, 0 }, + { 7396, 37, 68, 0, 0 }, + { 7408, 37, 68, 0, 0 }, + { 7421, 37, 68, 0, 0 }, + { 7438, 37, 68, 0, 0 }, + { 7452, 134, 165, 0, 0 }, + { 7464, 37, 68, 0, 0 }, + { 7476, 3839, 3860, 0, 0 }, + { 7490, 37, 68, 0, 0 }, + { 7508, 37, 68, 0, 0 }, + { 7524, 3839, 3860, 0, 0 }, + { 7539, 3919, 3940, 0, 0 }, + { 7557, 37, 68, 0, 0 }, + { 7571, 134, 165, 0, 0 }, + { 7584, 37, 68, 0, 0 }, + { 7601, 134, 165, 0, 0 }, + { 7616, 37, 68, 0, 0 }, + { 7630, 134, 165, 0, 0 }, + { 7646, 3839, 3860, 0, 0 }, + { 7663, 134, 165, 0, 0 }, + { 7679, 37, 68, 0, 0 }, + { 7692, 37, 68, 0, 0 }, + { 7707, 37, 68, 0, 0 }, + { 7721, 134, 165, 0, 0 }, + { 7736, 3919, 3940, 0, 0 }, + { 7753, 37, 68, 0, 0 }, + { 7767, 37, 68, 0, 0 }, + { 7781, 134, 165, 0, 0 }, + { 7799, 37, 68, 0, 0 }, + { 7813, 0, 3879, 0, 0 }, + { 7816, 0, 3879, 0, 0 }, + { 7824, 0, 0, 0, 0 }, + { 7828, 0, 0, 0, 0 }, + { 7834, 0, 0, 0, 0 }, + { 7840, 0, 0, 0, 0 }, + { 7845, 0, 0, 0, 0 }, + { 7855, 301, 301, 0, 0 }, + { 7859, 2285, 2309, 0, 0 }, + { 7868, 0, 0, 0, 0 }, + { 7876, 20, 20, 0, 0 }, + { 7896, 3959, 3959, 0, 0 }, + { 7910, 3977, 3977, 0, 0 }, + { 7927, 3999, 3999, 0, 0 }, + { 7940, 20, 20, 0, 0 }, + { 7954, 4018, 4018, 0, 0 }, + { 7971, 4051, 4051, 0, 0 }, + { 7983, 4067, 4067, 0, 0 }, + { 7999, 4081, 4105, 0, 0 }, + { 8016, 20, 20, 0, 0 }, + { 8031, 4127, 4127, 0, 0 }, + { 8046, 2989, 3008, 0, 0 }, + { 8051, 2501, 2522, 0, 0 }, + { 8058, 560, 779, 0, 0 }, + { 8066, 3039, 3039, 0, 0 }, + { 8072, 4141, 4141, 0, 0 }, + { 8082, 134, 165, 0, 0 }, + { 8088, 37, 68, 0, 0 }, + { 8092, 733, 733, 0, 0 }, + { 8096, 733, 756, 0, 0 }, + { 8104, 912, 934, 0, 0 }, + { 8121, 1075, 1105, 0, 0 }, + { 8136, 582, 604, 0, 0 }, + { 8151, 1616, 1642, 0, 0 }, + { 8154, 4163, 4185, 0, 0 }, + { 8162, 733, 756, 0, 0 }, + { 8169, 2145, 2165, 0, 0 }, + { 8173, 912, 934, 0, 0 }, + { 8181, 4207, 4227, 0, 0 }, + { 8194, 1616, 1642, 0, 0 }, + { 8211, 3732, 3732, 0, 0 }, + { 8232, 4163, 4185, 0, 0 }, + { 8248, 4247, 4247, 0, 0 }, + { 8262, 3578, 3606, 0, 0 }, + { 8277, 4258, 4280, 0, 0 }, + { 8291, 4300, 4300, 0, 0 }, + { 8309, 4321, 4321, 0, 0 }, + { 8325, 4334, 4353, 0, 0 }, + { 8338, 4370, 4370, 0, 0 }, + { 8355, 4382, 3692, 0, 0 }, + { 8373, 4397, 4397, 0, 0 }, + { 8389, 4410, 4410, 0, 0 }, + { 8409, 4431, 3722, 0, 0 }, + { 8422, 301, 301, 0, 0 }, + { 8439, 301, 301, 0, 0 }, + { 8456, 4300, 4300, 0, 0 }, + { 8471, 4454, 4454, 0, 0 }, + { 8490, 4472, 4472, 0, 0 }, + { 8505, 4141, 4141, 0, 0 }, + { 8523, 4141, 4141, 0, 0 }, + { 8538, 4484, 4484, 0, 0 }, + { 8556, 4499, 4499, 0, 0 }, + { 8571, 4528, 4528, 0, 0 }, + { 8585, 4539, 4539, 0, 0 }, + { 8598, 4549, 4578, 0, 0 }, + { 8614, 4607, 4635, 0, 0 }, + { 8629, 4499, 4499, 0, 0 }, + { 8647, 4661, 4661, 0, 0 }, + { 8661, 4672, 4672, 0, 0 }, + { 8678, 4686, 4686, 0, 0 }, + { 8694, 4686, 4686, 0, 0 }, + { 8709, 4699, 4699, 0, 0 }, + { 8730, 4721, 4748, 0, 0 }, + { 8748, 4431, 3722, 0, 0 }, + { 8763, 4499, 4499, 0, 0 }, + { 8777, 4773, 4773, 0, 0 }, + { 8792, 4785, 4785, 0, 0 }, + { 8807, 4806, 4826, 0, 0 }, + { 8825, 4247, 4247, 0, 0 }, + { 8838, 4844, 4844, 0, 0 }, + { 8851, 4861, 4861, 0, 0 }, + { 8866, 4247, 4247, 0, 0 }, + { 8878, 37, 68, 0, 0 }, + { 8885, 194, 225, 0, 0 }, + { 8894, 2905, 2926, 0, 0 }, + { 8898, 2778, 2860, 0, 0 }, + { 8902, 2881, 2881, 0, 0 }, + { 8912, 2411, 2434, 0, 0 }, + { 8919, 3812, 3812, 0, 0 }, + { 8923, 361, 382, 0, 0 }, + { 8933, 301, 331, 0, 0 }, + { 8945, 733, 733, 0, 0 }, + { 8956, 582, 604, 0, 0 }, + { 8967, 560, 779, 0, 0 }, + { 8983, 560, 779, 0, 0 }, + { 8994, 301, 301, 0, 0 }, + { 9004, 582, 604, 0, 0 }, + { 9022, 560, 779, 0, 0 }, + { 9034, 733, 756, 0, 0 }, + { 9046, 912, 934, 0, 0 }, + { 9057, 4499, 4499, 0, 0 }, + { 9066, 3812, 3812, 0, 0 }, + { 9070, 3812, 3812, 0, 0 }, + { 9080, 3919, 3940, 0, 0 }, + { 9085, 194, 225, 0, 0 }, + { 9089, 3812, 3812, 0, 0 }, +}; +#endif diff --git a/scripts/gen-timezone-names.mjs b/scripts/gen-timezone-names.mjs new file mode 100644 index 000000000..69aab583e --- /dev/null +++ b/scripts/gen-timezone-names.mjs @@ -0,0 +1,292 @@ +#!/usr/bin/env node +// Long time zone names for Date.prototype.toString, toTimeString and +// String(date), derived from the pinned Node release's ICU. +// +// node scripts/gen-timezone-names.mjs [--tzdata ] regenerate the table +// node scripts/gen-timezone-names.mjs [--tzdata ] --check verify only +// +// Node prints the zone's long English name for standard or daylight time +// after the GMT offset ("GMT-0500 (Central Daylight Time)"). The name +// depends only on the zone and on whether the instant is in daylight time +// (V8 maps instants outside 1970 to 2038-01-19 into that range), so each +// zone ID in the IANA tzdata (zones and links, read from tzdata.zi) is +// sampled once a day over that range with TZ set to it, and the two names +// Node prints are recorded. Where the name changes together with the UTC +// offset, the name at the larger offset is the daylight name. Zones +// without a CLDR name print Node's "GMT+hh:mm" fallback, which is recorded +// as is. +// +// The runtime takes daylight time from tm_isdst, except for two kinds of +// zones, which get the exact instants at which Node's name changes: +// zones whose tzdata rules use a negative daylight saving (Europe/Dublin's +// winter time, Morocco's Ramadan time), where ICU's data calls the larger +// offset daylight time, and zones that Node shows in daylight time through +// 2038 (British Columbia's permanent UTC-7, which tzdata names standard +// time). +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { availableParallelism } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const self = fileURLToPath(import.meta.url); +const root = fileURLToPath(new URL("..", import.meta.url)); +const out = join(root, "packages/runtime/src/scr_timezone_data.h"); + +// V8 names the zone of instants in [0, 2^31 - 1] seconds directly. +const FIRST_MS = 0; +const LAST_MS = 2147483647000; +const DAY_MS = 86400000; + +const zoneName = (ms) => { + const text = new Date(ms).toString(); + return text.slice(text.indexOf("(") + 1, -1); +}; + +if (process.argv.includes("--sample")) { + // One zone, selected by TZ: the first name printed, then every change of + // name with the exact instant (ms) and the UTC offsets (minutes) before + // and after it. + const changes = []; + let previous = null; + const visit = (ms) => { + const name = zoneName(ms); + const offset = -new Date(ms).getTimezoneOffset(); + if (previous === null) { + changes.push({ at: ms, name, offset }); + } else if (previous.name !== name) { + // The first second with the new name. + let lo = previous.ms / 1000; + let hi = ms / 1000; + while (hi - lo > 1) { + const mid = Math.floor((lo + hi) / 2); + if (zoneName(mid * 1000) === name) hi = mid; + else lo = mid; + } + changes.push({ at: hi * 1000, from: previous, name, offset }); + } + previous = { ms, name, offset }; + }; + for (let ms = FIRST_MS; ms < LAST_MS; ms += DAY_MS) visit(ms); + visit(LAST_MS); + process.stdout.write(JSON.stringify(changes)); + process.exit(0); +} + +const tzdataIndex = process.argv.indexOf("--tzdata"); +const tzdata = tzdataIndex >= 0 ? process.argv[tzdataIndex + 1] : "/usr/share/zoneinfo/tzdata.zi"; +const checkOnly = process.argv.includes("--check"); + +/** Zone and link names in tzdata.zi, and the zones that save a negative + * amount at some time from 1970 on. Links name their target. */ +function readTzdata(path) { + const text = readFileSync(path, "utf8"); + const zones = new Map(); + const negativeRules = new Set(); + const negativeZones = new Set(); + const year = (field) => (field === undefined ? Infinity : Number.parseInt(field, 10)); + let zone = null; + for (const line of text.split("\n")) { + const fields = line.trim().split(/\s+/); + if (line.startsWith("R ")) { + // R NAME FROM TO - IN ON AT SAVE LETTER, TO "o" (only) or "ma" (max). + zone = null; + const to = + fields[3] === "o" ? year(fields[2]) : fields[3] === "ma" ? Infinity : year(fields[3]); + if (fields[8]?.startsWith("-") && to >= 1970) negativeRules.add(fields[1]); + continue; + } + if (line.startsWith("L ")) { + zone = null; + zones.set(fields[2], fields[1]); + continue; + } + // Z NAME STDOFF RULES FORMAT [UNTIL], continued by STDOFF RULES FORMAT [UNTIL]. + let rest; + if (line.startsWith("Z ")) { + zone = fields[1]; + zones.set(zone, zone); + rest = fields.slice(2); + } else if (zone && fields.length >= 3 && !line.startsWith("#")) { + rest = fields; + } else { + zone = null; + continue; + } + const [, rule, , until] = rest; + if (year(until) >= 1970 && (negativeRules.has(rule) || /^-\d/.test(rule))) + negativeZones.add(zone); + } + const version = /^# version (\S+)/.exec(text)?.[1] ?? "unknown"; + return { zones, negativeZones, version }; +} + +/** The zone's [standard, daylight] names as Node prints them: at a change + * of name that also changes the UTC offset, the name printed at the larger + * offset is the daylight name; the name chosen at most such changes wins. */ +function classify(id, changes) { + const names = new Set(changes.map((c) => c.name)); + if (names.size > 2) throw new Error(`${id}: more than two names`); + if (names.size === 1) { + const [name] = names; + return [name, name]; + } + // Zones that moved between offsets keep one daylight name, so the vote + // can include a transition that only changed the standard offset. + const votes = new Map(); + for (const c of changes) { + if (!c.from || c.from.offset === c.offset) continue; + const name = c.offset > c.from.offset ? c.name : c.from.name; + votes.set(name, (votes.get(name) ?? 0) + 1); + } + const ranked = [...votes].sort((a, b) => b[1] - a[1]); + if (ranked.length === 0 || (ranked.length === 2 && ranked[0][1] === ranked[1][1])) + throw new Error(`${id}: no daylight name`); + const daylight = ranked[0][0]; + const standard = [...names].find((n) => n !== daylight); + return [standard, daylight]; +} + +function sample(id) { + return new Promise((resolve, reject) => { + const env = { ...process.env, TZ: id, LANG: "en_US.UTF-8" }; + delete env.LC_ALL; + delete env.LC_MESSAGES; + const child = spawn(process.execPath, [self, "--sample"], { env }); + let text = ""; + child.stdout.on("data", (chunk) => (text += chunk)); + child.on("error", reject); + child.on("close", (code) => + code === 0 ? resolve(JSON.parse(text)) : reject(new Error(`${id}: sampler exited ${code}`)), + ); + }); +} + +const { zones, negativeZones, version } = readTzdata(tzdata); +const ids = [...zones.keys()] + .filter((id) => { + try { + new Intl.DateTimeFormat("en-US", { timeZone: id }); + return true; + } catch { + return false; + } + }) + .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + +const samples = new Map(); +let next = 0; +await Promise.all( + Array.from({ length: availableParallelism() }, async () => { + while (next < ids.length) { + const id = ids[next++]; + samples.set(id, await sample(id)); + } + }), +); + +// Pools of NUL-terminated strings, addressed by byte offset. +function pool() { + const offsets = new Map(); + let text = ""; + return { + add(s) { + if (!offsets.has(s)) { + offsets.set(s, Buffer.byteLength(text)); + text += s + "\0"; + } + return offsets.get(s); + }, + get text() { + return text; + }, + }; +} +const idPool = pool(); +const namePool = pool(); +const changeSeconds = []; +const rows = ids.map((id) => { + const changes = samples.get(id); + const [standard, daylight] = classify(id, changes); + // Exact daylight-time changes, as epoch seconds at which daylight time + // starts or ends; INT32_MIN first when 1970 starts in daylight time. + let first = 0; + let count = 0; + // Daylight time with no change in the last year sampled. + const last = changes.at(-1); + const permanent = last.name === daylight && LAST_MS - last.at > 366 * DAY_MS; + if (standard !== daylight && (negativeZones.has(zones.get(id)) || permanent)) { + first = changeSeconds.length; + if (changes[0].name === daylight) changeSeconds.push("INT32_MIN"); + for (const c of changes.slice(1)) changeSeconds.push(String(c.at / 1000)); + count = changeSeconds.length - first; + } + return ` { ${idPool.add(id)}, ${namePool.add(standard)}, ${namePool.add(daylight)}, ${first}, ${count} },`; +}); +// The runtime formats into a fixed buffer. +for (const [standard, daylight] of ids.map((id) => classify(id, samples.get(id)))) { + if (Buffer.byteLength(standard) > 64 || Buffer.byteLength(daylight) > 64) + throw new Error(`zone name longer than 64 bytes: ${standard} / ${daylight}`); +} +for (const p of [idPool, namePool]) { + if (Buffer.byteLength(p.text) > 0xffff) throw new Error("string pool exceeds 16-bit offsets"); +} + +const cString = (text) => + text + .split("\0") + .slice(0, -1) + .map((s) => ` "${s.replace(/[\\"]/g, "\\$&")}\\0"`) + .join("\n"); + +const wrap = (items) => { + const lines = []; + let line = " "; + for (const item of items) { + if (line.length + item.length + 2 > 100) { + lines.push(line); + line = " "; + } + line += ` ${item},`; + } + if (line.trim()) lines.push(line); + return lines.join("\n"); +}; + +const header = `/* Generated by scripts/gen-timezone-names.mjs from Node ${process.version} + * (ICU ${process.versions.icu}, CLDR ${process.versions.cldr}) and tzdata ${version}. Do not edit. + * Each IANA zone or link ID maps to the long English names Node prints for + * its standard and daylight time. A row with change_count > 0 takes + * daylight time from scr_tz_daylight_changes[first_change ...]: the epoch + * seconds at which daylight time starts or ends between 1970 and + * 2038-01-19 (INT32_MIN first when 1970 starts in daylight time). Rows are + * sorted by ID. */ +#ifndef SCR_TIMEZONE_DATA_H +#define SCR_TIMEZONE_DATA_H +#include +typedef struct { uint16_t id, standard, daylight, first_change, change_count; } ScrTzNames; +static const char scr_tz_ids[] = +${cString(idPool.text)}; +static const char scr_tz_name_text[] = +${cString(namePool.text)}; +static const int32_t scr_tz_daylight_changes[] = { +${wrap(changeSeconds)} +}; +static const ScrTzNames scr_tz_names[] = { +${rows.join("\n")} +}; +#endif +`; + +if (checkOnly) { + const current = readFileSync(out, "utf8"); + if (current !== header) { + process.stderr.write(`${out} is out of date; run node scripts/gen-timezone-names.mjs\n`); + process.exit(1); + } + process.stdout.write(`${out} is up to date (${ids.length} zones)\n`); +} else { + await writeFile(out, header); + process.stdout.write(`wrote ${out} (${ids.length} zones)\n`); +} diff --git a/tests/corpus/5963-date-to-string.ts b/tests/corpus/5963-date-to-string.ts new file mode 100644 index 000000000..3b3024f3c --- /dev/null +++ b/tests/corpus/5963-date-to-string.ts @@ -0,0 +1,93 @@ +// Date.prototype.toString, toDateString and toTimeString, String(date), +// template literals and string concatenation print local time with the +// GMT offset and the zone's long name, as Node does. Each write to +// process.env.TZ changes the zone for later conversions and getters. +function at(ms: number): Date { + return new Date(ms); +} +const winter = at(Date.UTC(2026, 0, 15, 12, 30, 45, 678)); +const summer = at(Date.UTC(2026, 6, 15, 3, 4, 5)); +const epoch = at(0); +const beforeEpoch = at(Date.UTC(1950, 5, 1, 18)); +const farFuture = at(Date.UTC(2100, 6, 4, 9)); +const yearZero = at(-62167219200000); +const maxDate = at(8.64e15); +const minDate = at(-8.64e15); +const invalid = new Date(NaN); + +function show(zone: string): void { + process.env.TZ = zone; + console.log(`== ${zone}`); + console.log(winter.toString()); + console.log(String(summer)); + console.log(`${epoch} | ${beforeEpoch}`); + console.log("far: " + farFuture); + console.log(winter.toDateString(), "/", winter.toTimeString()); + console.log(summer.toDateString(), "/", summer.toTimeString()); + console.log(summer.getHours(), summer.getTimezoneOffset()); +} + +for (const zone of [ + "America/Chicago", + "America/New_York", + "America/St_Johns", + "Europe/Dublin", + "Europe/London", + "Europe/Berlin", + "Asia/Kolkata", + "Asia/Kathmandu", + "Asia/Tokyo", + "Australia/Lord_Howe", + "Australia/Sydney", + "Pacific/Chatham", + "America/Sao_Paulo", + "UTC", + "Etc/GMT+5", + "Etc/GMT-14", + "US/Pacific", + ":Asia/Seoul", +]) { + show(zone); +} + +// Daylight-time edges: the instants before and at a change. +process.env.TZ = "America/Chicago"; +const spring = Date.UTC(2026, 2, 8, 8); +const autumn = Date.UTC(2026, 10, 1, 7); +console.log(new Date(spring - 1000).toString(), "|", new Date(spring).toString()); +console.log(new Date(autumn - 1000).toTimeString(), "|", new Date(autumn).toTimeString()); +process.env.TZ = "Europe/Dublin"; +console.log(new Date(Date.UTC(2026, 2, 29, 0, 59, 59)).toString()); +console.log(new Date(Date.UTC(2026, 2, 29, 1)).toString()); +process.env.TZ = "Australia/Sydney"; +console.log(new Date(Date.UTC(2026, 3, 4, 15, 59, 59)).toString()); +console.log(new Date(Date.UTC(2026, 3, 4, 16)).toString()); + +// Extreme and invalid time values. +process.env.TZ = "UTC"; +console.log(maxDate.toString(), "|", minDate.toDateString()); +console.log(yearZero.toString(), "|", at(Date.UTC(-1, 11, 31, 23)).toDateString()); +console.log(at(Date.UTC(-12345, 0, 1)).toDateString(), "|", at(Date.UTC(12345, 0, 1)).toDateString()); +console.log(invalid.toString(), invalid.toDateString(), invalid.toTimeString(), `${invalid}`, String(invalid)); + +// A Date held as unknown converts the same way. +process.env.TZ = "Asia/Kolkata"; +const held: unknown = winter; +console.log(String(held), `${held}`); + +// Values that are not zone IDs and unset TZ. +for (const zone of ["Not/A_Zone", ""]) { + process.env.TZ = zone; + console.log(JSON.stringify(zone), winter.toString()); +} +delete process.env.TZ; +process.env.TZ = "UTC"; +console.log(winter.toString()); + +// Formatting functions and fields. +function stamp(label: string, d: Date): string { + return `${label}: ${d.toDateString()} at ${d.toTimeString().slice(0, 8)}`; +} +const event = { name: "launch", when: summer }; +process.env.TZ = "America/New_York"; +console.log(stamp(event.name, event.when), "|", event.when.toString().length); diff --git a/tests/corpus/5964-date-dynamic-conversions.js b/tests/corpus/5964-date-dynamic-conversions.js new file mode 100644 index 000000000..c2e5aff0d --- /dev/null +++ b/tests/corpus/5964-date-dynamic-conversions.js @@ -0,0 +1,29 @@ +// Dates held in untyped JavaScript values convert like Node's: string +// conversions use toString() (String, templates, +, ==, join), number +// conversions use the time value (Number, BigInt, unary +, -, *, <, >=), +// and toString, toDateString and toTimeString are callable as methods and +// as function values. +process.env.TZ = "America/Chicago"; +const date = new Date(Date.UTC(2026, 9, 18, 14, 12, 30)); +const held = [date, new Date(NaN), new Date(0)]; +const x = /** @type {any} */ (held[0]); +const bad = /** @type {any} */ (held[1]); +const zero = /** @type {any} */ (held[2]); + +console.log(String(x)); +console.log(`${x} / ${bad}`); +console.log(x.toString(), "|", x.toDateString(), "|", x.toTimeString()); +console.log(held.join(" ; ")); +console.log(x + 1, "|", 1 + x, "|", x + ""); +console.log(+x, x - 0, x * 1, Number(x), Number(bad), BigInt(zero)); +console.log(x > 5, x < zero, x >= x, x <= 1792332720000, bad < x, bad >= x); +console.log(x == x.toString(), x == 1792332750000, zero == "0"); + +const toTime = x.toTimeString; +const toDate = x.toDateString; +console.log(toTime.call(x), "|", toDate.call(zero)); + +process.env.TZ = "Asia/Kathmandu"; +console.log(String(x), "|", [x, zero].toString()); +process.env.TZ = "Europe/Dublin"; +console.log(`${zero}`, "|", zero.toTimeString()); diff --git a/tests/diagnostics/date-formatters.ts b/tests/diagnostics/date-formatters.ts index 6a2cb57a7..1ec1cacc6 100644 --- a/tests/diagnostics/date-formatters.ts +++ b/tests/diagnostics/date-formatters.ts @@ -1,5 +1,5 @@ // Unsupported formatters must reach the Date method diagnostic, including // names inherited from Object.prototype by the compiler's dispatch table. const date = new Date(0); -date.toString(); +date.toUTCString(); date.toLocaleString(); diff --git a/tests/harness/__snapshots__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..6fbe82dd6 100644 --- a/tests/harness/__snapshots__/date-formatters.ts.txt +++ b/tests/harness/__snapshots__/date-formatters.ts.txt @@ -1,17 +1,17 @@ -date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the standard library types but has no scriptc lowering yet +date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toUTCString' is part of the standard library types but has no scriptc lowering yet 3 | const date = new Date(0); - 4 | date.toString(); - | ^~~~~~~~~~~~~~~ + 4 | date.toUTCString(); + | ^~~~~~~~~~~~~~~~~~ 5 | date.toLocaleString(); - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet - 4 | date.toString(); + 4 | date.toUTCString(); 5 | date.toLocaleString(); | ^~~~~~~~~~~~~~~~~~~~~ 6 | - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering \ No newline at end of file diff --git a/tests/harness/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..34e6d5a3c 100644 --- a/tests/harness/__snapshots__/stdlib-fence.ts.txt +++ b/tests/harness/__snapshots__/stdlib-fence.ts.txt @@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity - hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering + hint: getTime(), valueOf(), toISOString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters, toUTCString() and locale formatters have no lowering stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 8131c1cd2..9fd80a3fd 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2190,6 +2190,8 @@ "5960-generic-mapped-types.ts": 2540, "5961-frame-spread-updates.ts": 3260, "5962-frame-tuples.ts": 3208, + "5963-date-to-string.ts": 2355, + "5964-date-dynamic-conversions.js": 2307, "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, @@ -5249,6 +5251,8 @@ "5960-generic-mapped-types.ts": 3300, "5961-frame-spread-updates.ts": 3732, "5962-frame-tuples.ts": 3372, + "5963-date-to-string.ts": 3199, + "5964-date-dynamic-conversions.js": 3084, "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, From c48983889c5f509b7c35bda0b656e2b251fceaf0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:35:23 -0500 Subject: [PATCH 41/67] Clear timers through handles that may be undefined or null clearTimeout, clearInterval and clearImmediate refused a handle typed `Timeout | undefined` or `Timeout | null`, the type of a timer variable that starts empty (`let timer: ReturnType | undefined`) and of `handles[i]!` reads. Such a handle now passes id 0 when it is absent, which matches no timer, so the clear does nothing, as in Node. --- .../src/frontend/lowering/lower-timers.ts | 28 ++++-- .../5684-clear-optional-timer-handles.ts.json | 6 ++ .../5684-clear-optional-timer-handles.ts | 89 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json create mode 100644 tests/corpus/5684-clear-optional-timer-handles.ts diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 44b2b6a1d..c2a54dfc0 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -6,6 +6,7 @@ import { canBoxFuncIntoDyn, DYN, F64, + isUnitType, VOID, type IrExpr, type IrType, @@ -299,6 +300,19 @@ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLo return null; } +/** A clear's handle as the f64 id it passes to the runtime. A handle that + * may be undefined or null (`let timer: Timeout | undefined`) passes 0 + * when absent: no timer has id 0, so the clear does nothing, as Node's + * clearTimeout(undefined) does. Other handle types are returned as they + * are. */ +function optionalTimerHandle(lowerer: Lowerer, handle: IrExpr, loc: SrcLoc): IrExpr { + if (handle.type.kind !== "union") return handle; + const arms = lowerer.unions.get(handle.type.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "f64") return handle; + return { kind: "nullish", left: handle, right: numLit(0, loc), type: F64, loc }; +} + const MOCKABLE_TIMER_FNS: ReadonlySet = new Set([ "timers.setTimeout", "timers.setTimeoutHandle", @@ -392,8 +406,7 @@ function lowerTimerCall( return { kind: "libCall", fn: "timers.setTimeout", args: [cb, ms], type: VOID, loc }; } // clearTimeout(handle): shares the interval clear (the handle ids - // share one space). A `Timeout | null` handle narrows first, like - // clearInterval. + // share one space). An optional handle clears nothing when absent. if (member === "clearTimeout") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; @@ -419,11 +432,12 @@ function lowerTimerCall( loc, }; } + handle = optionalTimerHandle(lowerer, handle, loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the Timeout setTimeout returned (narrow 'Timeout | null' first)", + "the handle is the Timeout setTimeout returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearTimeout", args: [handle], type: VOID, loc }; @@ -434,12 +448,12 @@ function lowerTimerCall( if (expr.arguments.length !== 1) { lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr); } - const handle = lowerer.lowerExpr(expr.arguments[0]!); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearInterval of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the number setInterval returned (narrow `number | null` first)", + "the handle is the Timeout setInterval returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearInterval", args: [handle], type: VOID, loc }; @@ -478,12 +492,12 @@ function lowerTimerCall( if (expr.arguments.length !== 1) { lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr); } - const handle = lowerer.lowerExpr(expr.arguments[0]!); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearImmediate of '${lowerer.fmt(handle.type)}' handles`, expr.arguments[0]!, - "the handle is the Immediate setImmediate returned (narrow 'Immediate | undefined' first)", + "the handle is the Immediate setImmediate returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearImmediate", args: [handle], type: VOID, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json new file mode 100644 index 000000000..307a74fa9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5684-clear-optional-timer-handles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5684-clear-optional-timer-handles.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5684-clear-optional-timer-handles.ts b/tests/corpus/5684-clear-optional-timer-handles.ts new file mode 100644 index 000000000..45a2f48aa --- /dev/null +++ b/tests/corpus/5684-clear-optional-timer-handles.ts @@ -0,0 +1,89 @@ +// clearTimeout, clearInterval and clearImmediate on handles that may be +// undefined or null: an absent handle clears nothing, a present one clears +// its timer, as in Node. Covers a debounce that re-arms through +// `Timeout | undefined`, array elements read with `!`, intervals that +// clear themselves through an optional handle, and immediates. + +const log: string[] = []; + +// A debounce: each call clears the pending timer, which is undefined at first. +let pending: ReturnType | undefined; +function debounce(label: string, ms: number): void { + clearTimeout(pending); + pending = setTimeout(() => { + log.push(`debounced ${label}`); + pending = undefined; + }, ms); +} +debounce("a", 20); +debounce("b", 20); +debounce("c", 5); + +// Null-initialized handles, cleared before and after they are set. +let retry: ReturnType | null = null; +clearTimeout(retry); +retry = setTimeout(() => log.push("retry fired"), 1); +let cancelled: ReturnType | null = setTimeout(() => log.push("cancelled fired"), 1); +clearTimeout(cancelled); +cancelled = null; +clearTimeout(cancelled); + +// Handles kept in an array, cleared through `!` element reads. +const handles = [setTimeout(() => log.push("kept 0"), 2)]; +for (let i = 1; i < 6; i++) handles.push(setTimeout(() => log.push(`kept ${i}`), 2 + i)); +for (let i = 1; i < handles.length; i += 2) clearTimeout(handles[i]!); + +// An interval that clears itself through its optional handle. +let ticks = 0; +let ticker: ReturnType | undefined; +clearInterval(ticker); +ticker = setInterval(() => { + ticks++; + if (ticks === 3) { + clearInterval(ticker); + ticker = undefined; + log.push(`ticker stopped at ${ticks}`); + } +}, 1); +let idle: ReturnType | null = null; +clearInterval(idle); + +// Immediates: an optional handle, cleared while pending and when absent. +let soon: ReturnType | undefined = setImmediate(() => log.push("soon fired")); +clearImmediate(soon); +soon = undefined; +clearImmediate(soon); +let later: ReturnType | null = setImmediate(() => log.push("later fired")); +const keep = later; +later = null; +clearImmediate(later); +console.log("kept immediate", keep !== null); + +// A class that owns an optional timer. +class Poller { + timer: ReturnType | undefined = undefined; + runs = 0; + start(): void { + this.stop(); + this.timer = setTimeout(() => { + this.runs++; + log.push(`poller run ${this.runs}`); + this.timer = undefined; + }, 3); + } + stop(): void { + clearTimeout(this.timer); + this.timer = undefined; + } +} +const poller = new Poller(); +poller.stop(); +poller.start(); +poller.start(); + +setTimeout(() => { + clearTimeout(pending); + clearInterval(ticker); + console.log(log.sort().join("\n")); + console.log("pending", pending === undefined, "ticker", ticker === undefined, "runs", poller.runs); +}, 60); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index a3ee81f9b..979c6ace1 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2004,6 +2004,7 @@ "5681-array-pop-and-length.ts": 3754, "5682-publish-wide-objects.ts": 3326, "5683-date-local-fields-tz.ts": 3848, + "5684-clear-optional-timer-handles.ts": 3989, "5700-cycle-release-retained.ts": 2156, "5701-cycle-release-old-edges.ts": 2601, "5702-publish-candidate-buffers.ts": 3469, @@ -5068,6 +5069,7 @@ "5681-array-pop-and-length.ts": 3216, "5682-publish-wide-objects.ts": 3376, "5683-date-local-fields-tz.ts": 3911, + "5684-clear-optional-timer-handles.ts": 1757, "5700-cycle-release-retained.ts": 3414, "5701-cycle-release-old-edges.ts": 4210, "5702-publish-candidate-buffers.ts": 3848, From 076d3e01fbf7bc0d9a74aa15710a50c95f4c98f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:46:53 -0500 Subject: [PATCH 42/67] List the Date string formatters in the library profile's Date family The determinism fence over stdlib.date. now covers toString, toDateString and toTimeString, which read the host time zone. --- tests/harness/library-profile.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..18abeecf8 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -551,7 +551,10 @@ describe("library profile fences", () => { "stdlib.date.getUTCSeconds", "stdlib.date.now", "stdlib.date.parse", + "stdlib.date.toDateString", "stdlib.date.toISOString", + "stdlib.date.toString", + "stdlib.date.toTimeString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); From 2ed5136923f5b79899298112a9e3d7b79220cc67 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:55:25 -0500 Subject: [PATCH 43/67] Convert class instances to strings and numbers through their own methods String(x), template literals, `+` with a string and Number(x) or unary plus on a program class instance failed with SC2001 ("values of type 'Money' cannot be compiled yet"), even when the class defines toString. They now run Node's ToPrimitive protocol through the class's methods: - `[Symbol.toPrimitive](hint)` when the class defines it, with the hint "string" for String(x) and template literals, "default" for `+`, and "number" for Number(x) and unary plus. - Otherwise toString (string hint) or valueOf and then toString (default and number hints), and "[object Object]" (NaN as a number) when the class defines neither. - The method dispatches like a call of it. Subclasses that convert through a different method are tested first with instanceof, so an array of base-class values converts each element by its own class. - Unions of class instances and primitives (`Box | undefined`) convert each arm as it would alone. Conversions the program can change at run time keep a named refusal: a toString or valueOf instance property or prototype write, a Symbol.toStringTag, and conversion methods that can return an object. Builtin classes and their subclasses keep their existing conversions. --- .../frontend/lowering/class-to-primitive.ts | 325 ++++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 4 + .../src/frontend/lowering/lower-exprs.ts | 34 +- .../corpus/6053-class-to-primitive.ts.json | 6 + .../corpus/6054-js-class-to-primitive.js.json | 6 + tests/corpus/6053-class-to-primitive.ts | 125 +++++++ tests/corpus/6054-js-class-to-primitive.js | 15 + tests/harness/corpus-costs.json | 4 + 8 files changed, 513 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/class-to-primitive.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json create mode 100644 tests/corpus/6053-class-to-primitive.ts create mode 100644 tests/corpus/6054-js-class-to-primitive.js diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts new file mode 100644 index 000000000..516a62f3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -0,0 +1,325 @@ +import { varRef } from "../../ir/build.js"; +import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; +import { isClassCallback } from "./class-callbacks.js"; +import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; + +/** The ToPrimitive hint of a conversion: `String(x)` and template literals + * take "string", `+` "default", `Number(x)` "number". */ +export type ConversionHint = "string" | "default" | "number"; +export type StringConversionHint = "string" | "default"; + +type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; + +/** How an instance of one class converts: through one of its methods, or + * through Object.prototype.toString ("[object Object]"). */ +type Plan = { method: ConversionMethod; ret: IrType } | { method: null }; + +const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); + +/** ToString(ToPrimitive(value, hint)) of a program class instance, as Node + * computes it: `[Symbol.toPrimitive](hint)` when the class defines it, then + * `toString` (hint "string") or `valueOf` and then `toString` (hint + * "default"), and "[object Object]" when the class defines neither. The + * methods dispatch like calls of them; subclasses that convert through a + * different method are tested first. Null for values whose class is not a + * plain program class (builtin classes and their subclasses keep their own + * conversions). */ +export function classToStringConversion( + lowerer: Lowerer, + value: IrExpr, + hint: StringConversionHint, + node: ts.Node, +): IrExpr | null { + return classConversion(lowerer, value, hint, node, STRING, (primitive) => + lowerer.ensureString(primitive, node), + ); +} + +/** ToNumber(ToPrimitive(value, "number")) of a program class instance + * (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then + * `toString`, and NaN for "[object Object]". Null as for + * classToStringConversion. */ +export function classToNumberConversion( + lowerer: Lowerer, + value: IrExpr, + node: ts.Node, + operator: "Number" | "unary plus" = "Number", +): IrExpr | null { + return classConversion(lowerer, value, "number", node, F64, (primitive) => + numberOfPrimitive(lowerer, primitive, node, operator), + ); +} + +/** ToNumber of a primitive value: `Number(primitive)`, or unary plus, + * which throws for a BigInt instead of converting it. */ +function numberOfPrimitive( + lowerer: Lowerer, + value: IrExpr, + node: ts.Node, + operator: "Number" | "unary plus", +): IrExpr { + const loc = value.loc; + if (operator === "unary plus" && mayBeBigint(lowerer, value.type)) + lowerer.unsupported( + "SC1090", + node, + "unary plus of class instances whose conversion can return a bigint", + ); + switch (value.type.kind) { + case "f64": + return value; + case "string": + return { kind: "libCall", fn: "num.fromString", args: [value], type: F64, loc }; + case "bool": + return { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; + case "bigint": + return { kind: "libCall", fn: "bigint.toF64", args: [value], type: F64, loc }; + default: + if (!lowerer.dynConvertible(value.type)) + lowerer.unsupported("SC1090", node, "number conversions of this class's primitive value"); + return { + kind: "libCall", + fn: "dyn.numberConstructor", + args: [lowerer.coerceToExpected(value, DYN)], + type: F64, + loc, + }; + } +} + +function mayBeBigint(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "bigint") return true; + return ( + type.kind === "union" && + lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "bigint") === true + ); +} + +function classConversion( + lowerer: Lowerer, + value: IrExpr, + hint: ConversionHint, + node: ts.Node, + resultType: IrType, + finish: (primitive: IrExpr) => IrExpr, +): IrExpr | null { + if (value.type.kind !== "object") return null; + const info = lowerer.classes.get(value.type.className); + if (!info || !plainProgramClass(info)) return null; + const classes = subtree(lowerer, info); + for (const cls of classes) refuseReplacedConversions(lowerer, cls, node); + const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)])); + // A class whose plan differs from its base's starts a group; the deepest + // groups are tested first, so each instance takes its own class's plan. + const tops = classes + .filter((cls) => { + if (cls === info) return false; + const base = cls.base; + return base !== null && plans.has(base) && !samePlan(plans.get(base)!, plans.get(cls)!); + }) + .sort((a, b) => depth(b) - depth(a)); + const loc = value.loc; + const prefix: IrStmt[] = []; + let receiver = value; + if (tops.length > 0 && !isSafeToRepeat(value)) { + const local = lowerer.declareHiddenLocal("%converted", value.type); + prefix.push({ kind: "varDecl", localId: local.id, init: value, loc }); + receiver = varRef(local.id, value.type, loc); + } + const convert = (cls: ClassInfo, object: IrExpr): IrExpr => { + const plan = plans.get(cls)!; + if (plan.method === null) + return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); + const args: IrExpr[] = + plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : []; + return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc)); + }; + let result = convert(info, receiver); + for (let i = tops.length - 1; i >= 0; i--) { + const top = tops[i]!; + const narrowed: IrExpr = { + kind: "downcast", + value: receiver, + type: { kind: "object", className: top.def.name }, + loc, + }; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: receiver, className: top.def.name, type: BOOL, loc }, + then: convert(top, narrowed), + else_: result, + type: resultType, + loc, + }; + } + return prefix.length > 0 + ? { kind: "seqExpr", stmts: prefix, result, type: resultType, loc } + : result; +} + +/** A class of the program whose chain reaches no builtin base: its + * instances inherit Object.prototype's conversions. */ +function plainProgramClass(info: ClassInfo): boolean { + for (let cls: ClassInfo | null = info; cls; cls = cls.base) { + if ( + cls.def.runtime || + cls.builtinError || + cls.builtinEmitter || + cls.builtinStream || + cls.callableBase !== undefined + ) + return false; + } + return true; +} + +function subtree(lowerer: Lowerer, info: ClassInfo): ClassInfo[] { + const out: ClassInfo[] = []; + const visit = (cls: ClassInfo): void => { + out.push(cls); + for (const child of cls.subclasses) visit(child); + }; + visit(info); + return out.filter( + (cls) => cls === info || lowerer.classCanBeConstructed(cls) || cls.subclasses.length > 0, + ); +} + +function depth(info: ClassInfo): number { + let n = 0; + for (let cls = info.base; cls; cls = cls.base) n++; + return n; +} + +function samePlan(a: Plan, b: Plan): boolean { + return a.method === b.method; +} + +/** A conversion method the class declares or inherits (a field of that + * name is an instance property the program sets, refused separately). */ +function declared( + lowerer: Lowerer, + info: ClassInfo, + method: ConversionMethod, +): { ret: IrType } | null { + if (info.fields.has(method)) return null; + const found = findMethodOn(lowerer, info, method); + return found ? { ret: found.sig.ret } : null; +} + +function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan { + const exotic = declared(lowerer, info, "sym:toPrimitive"); + if (exotic) { + if (!primitive(lowerer, exotic.ret)) + refuse(lowerer, node, info, "a [Symbol.toPrimitive] method that can return an object"); + return { method: "sym:toPrimitive", ret: exotic.ret }; + } + // OrdinaryToPrimitive: "string" tries toString first, which every object + // has (Object.prototype.toString answers a string); "default" and + // "number" try valueOf first, whose Object.prototype version answers the + // object itself. + const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"]; + for (const method of order) { + const found = declared(lowerer, info, method); + if (!found) continue; + if (!primitive(lowerer, found.ret)) + refuse(lowerer, node, info, `a '${method}' method that can return an object`); + return { method, ret: found.ret }; + } + return { method: null }; +} + +function primitive(lowerer: Lowerer, type: IrType): boolean { + if (type.kind === "union") + return ( + lowerer.unions.get(type.unionId)?.arms.every((arm) => PRIMITIVE_KINDS.has(arm.kind)) === true + ); + return PRIMITIVE_KINDS.has(type.kind); +} + +/** Conversions the program can change at run time (an instance property + * of a method's name, a prototype write, a Symbol.toStringTag) would not be + * the class's declared methods. */ +function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.Node): void { + for (const method of ["toString", "valueOf"] as const) { + if (info.fields.has(method) || isClassCallback(lowerer, info, method)) + refuse(lowerer, node, info, `a '${method}' instance property`); + if (lowerer.prototypeMethodAccesses.has(method)) + refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`); + } + if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag")) + refuse(lowerer, node, info, "a Symbol.toStringTag"); +} + +function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never { + lowerer.unsupported( + "SC1090", + node, + `conversions of '${info.def.jsName ?? info.def.name}' instances to primitives (the class has ${what})`, + ); +} + +/** ToString(ToPrimitive(value, hint)) of a union whose arms are program + * class instances and primitives (`Box | undefined`): each arm converts as + * it would alone. Null for other unions. */ +export function classUnionToString( + lowerer: Lowerer, + value: IrExpr, + hint: StringConversionHint, + node: ts.Node, +): IrExpr | null { + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms || !arms.some((arm) => arm.kind === "object")) return null; + const convertible = (arm: IrType): boolean => { + if (PRIMITIVE_KINDS.has(arm.kind)) return true; + if (arm.kind !== "object") return false; + const info = lowerer.classes.get(arm.className); + return info !== undefined && plainProgramClass(info); + }; + if (!arms.every(convertible)) return null; + const loc = value.loc; + const prefix: IrStmt[] = []; + let stable = value; + if (!isSafeToRepeat(value)) { + const local = lowerer.declareHiddenLocal("%converted", value.type); + prefix.push({ kind: "varDecl", localId: local.id, init: value, loc }); + stable = varRef(local.id, value.type, loc); + } + const convertArm = (tag: number): IrExpr => { + const arm = arms[tag]!; + if (arm.kind === "undefinedT" || arm.kind === "nullT") + return { + kind: "strLit", + value: arm.kind === "nullT" ? "null" : "undefined", + type: STRING, + loc, + }; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: stable, type: arm, loc }; + if (arm.kind === "object") return classToStringConversion(lowerer, narrowed, hint, node)!; + return lowerer.ensureString(narrowed, node); + }; + let result = convertArm(arms.length - 1); + for (let tag = arms.length - 2; tag >= 0; tag--) { + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: stable, type: BOOL, loc }, + then: convertArm(tag), + else_: result, + type: STRING, + loc, + }; + } + return prefix.length > 0 ? { kind: "seqExpr", stmts: prefix, result, type: STRING, loc } : result; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 303bdf517..f431d4e54 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -157,6 +157,7 @@ import { } from "./lower-exprs.js"; import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { classToNumberConversion } from "./class-to-primitive.js"; import { hasRuntimeStatics } from "./class-runtime-statics.js"; import { httpClientFnBindingOf, @@ -2134,6 +2135,9 @@ function lowerNumberConstructorValue( }; const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc); if (optionalNumber) return optionalNumber; + // A program class instance converts through its own methods. + const classNumber = classToNumberConversion(lowerer, arg, argNode); + if (classNumber) return classNumber; const scalarUnionNumber = lowerScalarUnionNumber(lowerer, arg, argNode, loc); if (scalarUnionNumber) return scalarUnionNumber; return lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 558838c1f..fc820b4d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -221,6 +221,11 @@ import { } from "./expressions/optional-chains.js"; import { foldedStringKeyOf } from "./expressions/object-literals.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; +import { + classToNumberConversion, + classToStringConversion, + classUnionToString, +} from "./class-to-primitive.js"; import { fenceNodeModuleMutation, isNodeModuleValue, @@ -8107,7 +8112,7 @@ function lowerElementCompoundParts( const appended: IrExpr = { kind: "strConcat", left: oldValue, - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }; @@ -8125,7 +8130,7 @@ function lowerElementCompoundParts( computed = { kind: "strConcat", left: lowerer.ensureString(oldRef(), target), - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }; @@ -8876,6 +8881,16 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return lowerRecordPropertyKey(lowerer, key, node); } +/** An operand of `+` with a string: ToString of ToPrimitive with the + * default hint, which tries a class instance's valueOf before toString. */ +function concatOperand(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr { + return ( + classToStringConversion(lowerer, e, "default", node) ?? + classUnionToString(lowerer, e, "default", node) ?? + lowerer.ensureString(e, node) + ); +} + export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr { if (e.type.kind === "string") return e; if (e.type.kind === "url") @@ -8919,6 +8934,9 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr return errorToStringCall(lowerer, e); } } + // A program class instance converts through its own methods. + const converted = classToStringConversion(lowerer, e, "string", node); + if (converted) return converted; } if (e.type.kind === "union") { // `${u}` — the arm's ToString via a per-union interned helper @@ -8977,6 +8995,8 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr node, ); } + const viaClasses = classUnionToString(lowerer, e, "string", node); + if (viaClasses) return viaClasses; lowerer.unsupported( "SC1090", node, @@ -9450,6 +9470,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "string") { return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } + const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); + if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "+", [raw.type]); return operand; @@ -9918,7 +9940,7 @@ export function lowerCompoundValueToTarget( { kind: "strConcat", left: lowerer.ensureString(read, expr.left), - right: lowerer.ensureString(rhs, expr.right), + right: concatOperand(lowerer, rhs, expr.right), type: STRING, loc, }, @@ -10981,8 +11003,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr if (left.type.kind === "string" || right.type.kind === "string") { return { kind: "strConcat", - left: lowerer.ensureString(left, expr.left), - right: lowerer.ensureString(right, expr.right), + left: concatOperand(lowerer, left, expr.left), + right: concatOperand(lowerer, right, expr.right), type: STRING, loc, }; @@ -16310,7 +16332,7 @@ function lowerFieldCompoundValue( const numericRead = lowerOptionalNumber(lowerer, read, loc, access); const bigintValue = rhsNode ? bigintCompoundValue(op, read, rhs, loc) : null; if (op === "+" && read.type.kind === "string") { - appended = lowerer.ensureString(rhs, rhsNode ?? access); + appended = concatOperand(lowerer, rhs, rhsNode ?? access); value = { kind: "strConcat", left: read, right: appended, type: STRING, loc }; } else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") { value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc }; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json new file mode 100644 index 000000000..6c4f1aaea --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6053-class-to-primitive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6053-class-to-primitive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json new file mode 100644 index 000000000..50591191a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6054-js-class-to-primitive.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6054-js-class-to-primitive.js" + ], + "diags": [] +} diff --git a/tests/corpus/6053-class-to-primitive.ts b/tests/corpus/6053-class-to-primitive.ts new file mode 100644 index 000000000..c79a5ddcb --- /dev/null +++ b/tests/corpus/6053-class-to-primitive.ts @@ -0,0 +1,125 @@ +// String and number conversions of class instances run Node's ToPrimitive +// protocol through the class's own methods: String(x) and template +// literals use the string hint, `+` with a string the default hint, and +// Number(x) and unary plus the number hint. Classes without conversion +// methods convert to "[object Object]" and NaN. +class Money { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + toString(): string { + return `$${(this.cents / 100).toFixed(2)}`; + } + valueOf(): number { + return this.cents; + } +} +const m = new Money(1250); +console.log(String(m), `${m}`, "total " + m, m + "!"); +let s = "sum:"; +s += m; +console.log(s); +class Plain { + x = 1; +} +console.log(String(new Plain()), `${new Plain()}`, "p" + new Plain()); +class Base { + name(): string { + return "base"; + } +} +class Named extends Base { + toString(): string { + return "named " + this.name(); + } +} +class Deeper extends Named { + valueOf(): number { + return 7; + } +} +const items: Base[] = [new Base(), new Named(), new Deeper()]; +for (const item of items) console.log(String(item), `${item}`, "+" + item); +class Hinted { + [Symbol.toPrimitive](hint: string): string | number { + return hint === "number" ? 42 : `hint:${hint}`; + } +} +const h = new Hinted(); +console.log(String(h), `${h}`, "x" + h); +let calls = 0; +class Counted { + toString(): string { + calls++; + return "c" + calls; + } +} +const c = new Counted(); +console.log(`${c}-${c}`, calls); + +class Amount { + cents: number; + constructor(cents: number) { + this.cents = cents; + } + valueOf(): number { + return this.cents; + } +} +const amount = new Amount(1250); +console.log(Number(amount), +amount); +class Label { + toString(): string { + return "12.5"; + } +} +class Flag { + [Symbol.toPrimitive](hint: string): number | string { + return hint === "number" ? 3 : "flag"; + } +} +class Empty {} +console.log(Number(new Label()), +new Label(), Number(new Flag()), +new Flag(), `${new Flag()}`, Number(new Empty())); + +class Box { + value: T; + constructor(value: T) { + this.value = value; + } + toString(): string { + return `Box(${String(this.value)})`; + } + describe(): string { + return `I am ${this}`; + } +} +console.log(`${new Box(1)}`, String(new Box("s")), new Box(true).describe()); +abstract class Shape { + abstract area(): number; + toString(): string { + return `shape:${this.area()}`; + } +} +class Square extends Shape { + side: number; + constructor(side: number) { + super(); + this.side = side; + } + area(): number { + return this.side * this.side; + } +} +const shapes: Shape[] = [new Square(3)]; +console.log(shapes.map((shape) => `${shape}`).join(",")); +class Code { + valueOf(): string { + return "code-value"; + } +} +console.log(String(new Code()), "" + new Code()); +const maybe: Box | undefined = process.argv.length > 99 ? undefined : new Box(5); +console.log(`${maybe}`); +const pair: Code | number | null = process.argv.length > 99 ? null : new Code(); +console.log(`${pair}`, "p:" + pair); diff --git a/tests/corpus/6054-js-class-to-primitive.js b/tests/corpus/6054-js-class-to-primitive.js new file mode 100644 index 000000000..e5f47b233 --- /dev/null +++ b/tests/corpus/6054-js-class-to-primitive.js @@ -0,0 +1,15 @@ +// JavaScript class instances convert to strings through their own toString, +// and to "[object Object]" without one, as in Node. +class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + toString() { + return `(${this.x}, ${this.y})`; + } +} +const p = new Point(1, 2); +console.log(`${p}`, String(p), "p=" + p); +class Bare {} +console.log(`${new Bare()}`); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index b1e77984a..8c6307926 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2230,6 +2230,8 @@ "6043-string-index-narrowed-union.ts": 2983, "605-closures-forward-capture-tdz.ts": 3277, "6051-generic-class-factory-results.ts": 4363, + "6053-class-to-primitive.ts": 4385, + "6054-js-class-to-primitive.js": 2181, "6070-iterator-helpers-untyped.js": 4050, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, @@ -5297,6 +5299,8 @@ "6043-string-index-narrowed-union.ts": 3627, "605-closures-forward-capture-tdz.ts": 4638, "6051-generic-class-factory-results.ts": 46581, + "6053-class-to-primitive.ts": 36270, + "6054-js-class-to-primitive.js": 2958, "6070-iterator-helpers-untyped.js": 6900, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, From e8e432b6ddc0791f56c2ff84d92c8afb028e0c7e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 20:58:20 -0500 Subject: [PATCH 44/67] Clean up subtests cancelled by their parent's timeout node:test changes, matching Node 24.22: - A subtest cancelled because its parent timed out has its mocks restored and its mock timers reset when it is cancelled, as Node's postRun resets the test's mock tracker. Before, a mocked method stayed mocked for the tests that ran after it. - Its afterEach chain and t.after hooks run at the next turn, once the runner has moved on to the next test, as Node runs them when the cancelled test's own run resumes. Before, they never ran. The chain is copied when the test is cancelled, since its ancestors drop their hooks when they finish. - Its hooks are then dropped, so closures that capture its context do not keep it alive. The registration parent is still not restored when the cancelled body settles: it belongs to the test that runs then. The timeout-cancel-cleanup fixture records the mocked method and the hook order against the next test's body and compares them with Node, plain and with SCRIPTC_SAN=1. It fails without this change. --- packages/runtime/src/scr_test.c | 62 ++++++++++++++++++- .../node-test/cases/timeout-cancel-cleanup.ts | 44 +++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 4217169b8..00e609129 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -599,9 +599,59 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { scr_test_report_result(c); } +/* A cancelled subtest's afterEach chain and t.after hooks, on their own + * fiber: they start at the next turn, once the runner has moved on, as + * Node runs them when the cancelled test's own run resumes. The afterEach + * chain is copied when the test is cancelled, because its ancestors drop + * their hooks when they finish. Then the test's hooks are dropped, + * releasing closures that capture its context. */ +typedef struct { + ScrTestCtx *test; /* owned ref */ + ScrTestHooks each; /* the afterEach chain, innermost first; owned closures */ +} ScrTestCancelled; + +static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { + (void)self; + ScrTestCancelled *job = arg; + ScrTestCtx *c = job->test; + scr_await_hop(); /* the spawn runs the fiber at once; start at the next turn */ + for (size_t i = 0; i < job->each.n && !scr_exc_pending(); i++) + scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, NULL); + if (scr_exc_pending()) scr_exc_clear(); + scr_test_run_hooks_of(c, SCR_HOOK_AFTER, c); + if (scr_exc_pending()) scr_exc_clear(); + for (int k = 0; k < 4; k++) scr_test_hooks_free(&c->hooks[k]); + scr_test_hooks_free(&job->each); + scr_testctx_release(c); + free(job); +} + +static void scr_test_cancelled_hooks(ScrTestCtx *c) { + ScrTestCancelled *job = calloc(1, sizeof *job); + if (!job) scr_test_oom(); + job->test = scr_testctx_retain(c); + for (ScrTestCtx *p = c->parent; p; p = p->parent) { + ScrTestHooks *h = &p->hooks[SCR_HOOK_AFTER_EACH]; + for (size_t i = 0; i < h->n; i++) { + if (job->each.n == job->each.cap) { + job->each.cap = job->each.cap ? job->each.cap * 2 : 4; + job->each.v = realloc(job->each.v, job->each.cap * sizeof *job->each.v); + if (!job->each.v) scr_test_oom(); + } + job->each.v[job->each.n].cb = scr_closure_retain(h->v[i].cb); + job->each.v[job->each.n].flags = h->v[i].flags; + job->each.v[job->each.n].at = NULL; + job->each.n++; + } + } + scr_promise_release(scr_async_spawn(scr_test_cancelled_hooks_entry, job)); +} + /* A test that timed out (Node): its subtests that are still running are - * cancelled, innermost first, with their durations so far. Their bodies - * keep running unobserved and report nothing when they settle. */ + * cancelled, innermost first, with their durations so far. Each one's + * mocks are restored and its mock timers reset at once, and its afterEach + * chain and t.after hooks run once the runner yields. Their bodies keep + * running unobserved and report nothing when they settle. */ static void scr_test_cancel_running(ScrTestCtx *t) { for (size_t i = 0; i < t->nchildren; i++) { ScrTestCtx *c = t->children[i]; @@ -613,6 +663,9 @@ static void scr_test_cancel_running(ScrTestCtx *t) { c->dur_ms = scr_now_ms() - c->start_ms; c->state = SCR_TS_CANCELLED; scr_test_report_result(c); + scr_test_mocks_restore(&c->mocks, true); + scr_mt_reset(c->mock_timers); + scr_test_cancelled_hooks(c); } } @@ -725,7 +778,10 @@ static void scr_test_run_node(ScrTestCtx *t) { scr_test_call(t->cb, t->flags, t, t); if (t->state != SCR_TS_PENDING) { /* Its parent timed out and cancelled it while the body ran: the - * result is reported, and the runner has moved on. */ + * result is reported, its cleanup ran at the cancellation + * (scr_test_cancel_running), and the runner has moved on, so the + * registration parent is not restored either: it belongs to the + * test that runs now. */ if (scr_exc_pending()) scr_exc_clear(); if (t->concurrency > 1) scr_test_defer--; return; diff --git a/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts b/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts new file mode 100644 index 000000000..f9c0e726c --- /dev/null +++ b/tests/fixtures/node-test/cases/timeout-cancel-cleanup.ts @@ -0,0 +1,44 @@ +// @exit: 1 +// A subtest cancelled by its parent's timeout is cleaned up as Node does: +// its mocks are restored at once, and its afterEach chain and t.after +// hooks run once the runner has moved on to the next test. Its body keeps +// running and sees the restored method when it settles. +import { afterEach, test } from "node:test"; +import * as assert from "node:assert"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const api = { greet: (): string => "real" }; +const seen: string[] = []; + +afterEach((c) => { + seen.push(`root afterEach for ${c.name}`); +}); + +test("parent", { timeout: 30 }, async (t) => { + t.afterEach((c) => { + seen.push(`parent afterEach for ${c.name}`); + }); + await t.test("sub", async (t) => { + t.mock.method(api, "greet", () => "mocked"); + t.after(() => { + seen.push(`after hook of ${t.name}`); + }); + seen.push(`sub runs: ${api.greet()}`); + await sleep(200); + seen.push(`sub settles: ${api.greet()}`); + }); +}); + +test("next", async () => { + seen.push(`next starts: ${api.greet()}`); + await sleep(300); + assert.deepStrictEqual(seen, [ + "sub runs: mocked", + "root afterEach for parent", + "next starts: real", + "parent afterEach for sub", + "root afterEach for sub", + "after hook of sub", + "sub settles: real", + ]); +}); From 96f59b0f3f782d7f14aaf334648034dc23514017 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 21:53:14 -0500 Subject: [PATCH 45/67] Free errors that crossed into checked-dynamic code and attach Node's error properties The error identity cache (scr_dyn_from_error) keeps one checked-dynamic node per error so a crossing error compares reference-equal and keeps the properties set on it. It retained both sides for the whole process, so every error that crossed a checked-dynamic boundary (a caught error read through a cast, an error delivered to an untyped callback, a dns failure, a warning) stayed alive until exit: 100,000 caught AssertionErrors read through a cast grew the resident set by about 210 MiB. The cache now sweeps itself. An entry whose error and node are referenced only by the cache is unreachable (a node holds no reference to its error; a registered subclass box holds one, and a spawnSync error's `error` property refers to its own node), so a sweep releases both sides. Sweeps run when the cache doubles past the entries the last sweep kept, so they cost amortized constant time per crossing. The cache is per thread, and objects shared across threads are published (immortal), so a sweep never races another thread's reference. Live errors and live nodes keep their entries: identity, rethrowing the same instance, isNativeError and kind checks are unchanged. scr_error_set_prop sets an own property on an error's node; dns, assert, cipher, crypto, util.callbackify and warning errors use it for the properties they attach. util.inspect of a runtime error renders through its node: Node's own properties follow the bracket form in key order (a system error's errno, code, syscall and path, and the extra properties a throw site attached), and an AssertionError renders at depth 0 with long string actual/expected values shortened, as its custom inspect does. A node that contains itself renders as Node's /[Circular *1]. child_process errors carry Node's own properties: - execSync and execFileSync: status, signal, output, pid, stdout and stderr (Buffers, or strings with an encoding option); a spawn failure or timeout adds errno, syscall, path, spawnargs and error first. - execFile's callback and the promisified execFile/exec: code (the exit status, or null after a signal), killed, signal and cmd, with stdout and stderr when promisified; a spawn failure has errno, code, syscall, path, spawnargs and cmd. The sync forms echo the captured stderr before a timeout throw too, as Node's inheritStderr does. The cp.execSync libCall takes a flag for Buffer outputs. An error's own `stack` stays non-enumerable when assigned through its node. A worker's record-source table is freed when the worker exits. --- .../process-arguments-and-errors.mdx | 2 +- .../lowering/builtins/child-process-sync.ts | 6 + .../compiler/src/ir/builtin-signatures.ts | 9 +- .../5634-error-extra-props-inspect.ts.json | 6 + .../5635-error-extra-props-retry-loop.ts.json | 6 + .../corpus/5636-child-error-props.ts.json | 6 + packages/runtime/src/scr_assert.c | 17 +- packages/runtime/src/scr_async_dyn.c | 4 +- packages/runtime/src/scr_child.c | 377 +++++++++++++++--- packages/runtime/src/scr_child.h | 12 +- packages/runtime/src/scr_cipher.c | 10 +- packages/runtime/src/scr_dgram.c | 2 +- packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_inspect.c | 77 +++- packages/runtime/src/scr_inspect.h | 11 +- packages/runtime/src/scr_json.c | 137 ++++++- packages/runtime/src/scr_lib.c | 8 +- packages/runtime/src/scr_util_callable.c | 2 +- .../corpus/5634-error-extra-props-inspect.ts | 64 +++ .../5635-error-extra-props-retry-loop.ts | 31 ++ tests/corpus/5636-child-error-props.ts | 95 +++++ .../fixtures/error-cache-sweep/retry-loop.ts | 34 ++ tests/harness/corpus-costs.json | 6 + tests/harness/error-cache-sweep.test.ts | 38 ++ 24 files changed, 856 insertions(+), 108 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json create mode 100644 tests/corpus/5634-error-extra-props-inspect.ts create mode 100644 tests/corpus/5635-error-extra-props-retry-loop.ts create mode 100644 tests/corpus/5636-child-error-props.ts create mode 100644 tests/fixtures/error-cache-sweep/retry-loop.ts create mode 100644 tests/harness/error-cache-sweep.test.ts diff --git a/docs/content/limitations/process-arguments-and-errors.mdx b/docs/content/limitations/process-arguments-and-errors.mdx index d048b4154..69d7d9dc1 100644 --- a/docs/content/limitations/process-arguments-and-errors.mdx +++ b/docs/content/limitations/process-arguments-and-errors.mdx @@ -1,5 +1,5 @@ `process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. In programs that use `node:test`, a leading `--test-update-snapshots` argument is Node's snapshot update flag: it is kept out of `process.argv` and appears in `process.execArgv`, which is otherwise `[]`. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against. -Uncaught exceptions print `Uncaught ` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`. +Uncaught exceptions print `Uncaught ` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code` as typed members. The other own properties Node gives them, such as a system error's `errno`, `syscall`, and `path`, a child process error's `status`, `stdout`, and `stderr`, or an assertion error's `actual` and `expected`, are readable through an untyped view of the error (for example a cast of a caught value) and appear in `util.inspect` output. Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`. diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index a8dea8001..bfa2bc99a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -102,6 +102,8 @@ export function lowerExecSyncCall( let stdoutMode = 1; let stderrMode = 0; let stdinInherit = false; + // Without an encoding option, a failure's error carries Buffer outputs. + let encodingGiven = false; // A conditional env spread (`...(c ? { env: ... } : {})`): the call // itself splits into a ternary of the two env-nesses at the tail. let condEnvSpread: { cond: IrExpr; pairs: IrExpr; whenTrue: boolean } | null = null; @@ -134,6 +136,8 @@ export function lowerExecSyncCall( runtime.timeout, runtime.stdoutMode, runtime.stderrMode, + // The runtime options carry a utf8 encoding (others throw). + boolLit(false, loc), ], type: STRING, loc, @@ -251,6 +255,7 @@ export function lowerExecSyncCall( 'outputs are captured as utf8 — pass { encoding: "utf8" }', ); } + encodingGiven = true; break; } case "cwd": @@ -395,6 +400,7 @@ export function lowerExecSyncCall( timeout, numLit(stdoutMode + (stdinInherit ? 4 : 0), loc), numLit(stderrMode, loc), + boolLit(!encodingGiven, loc), ], type: STRING, loc, diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d2c503214..9467a2458 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2285,11 +2285,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ result: VOID, }, /** execFileSync/execSync as ONE entry (args: cmd, argv, shell, input, - * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode — see - * scr_runtime.h). Throws Node's exact errors: "Command failed: " + * cwd, hasEnv, envPairs, timeoutMs, stdoutMode, stderrMode, buffers — + * see scr_child.h). Throws Node's exact errors: "Command failed: " * (+ captured stderr) on non-zero exit or signal death, "spawnSync * ENOENT" on spawn failure, "spawnSync ETIMEDOUT" after - * the SIGTERM timeout. Result is the captured utf8 stdout (+1). */ + * the SIGTERM timeout, with Node's status, signal, output, pid, stdout + * and stderr properties (Buffer outputs when buffers is set: no + * encoding option). Result is the captured utf8 stdout (+1). */ "cp.execSync": { argTypes: [ STRING, @@ -2303,6 +2305,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ F64, F64, F64, + BOOL, ], result: STRING, }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json new file mode 100644 index 000000000..418b206b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5634-error-extra-props-inspect.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5634-error-extra-props-inspect.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json new file mode 100644 index 000000000..9fbf4c473 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5635-error-extra-props-retry-loop.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5635-error-extra-props-retry-loop.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json new file mode 100644 index 000000000..bf6db4e51 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5636-child-error-props.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5636-child-error-props.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 5a4769038..4917b1291 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -167,19 +167,16 @@ static ScrDyn *scr_assert_dyn_text(const char *text) { return d; } -/* Sets the properties on the error's view in Node's order. The values - * move in. */ +/* Sets the error's own properties in Node's order. The values move in. */ static void scr_assert_attach_values(ScrError *e, bool generated, ScrDyn *actual, ScrDyn *expected, ScrDyn *op, ScrDyn *diff) { - ScrDyn *view = scr_dyn_from_error(e); - scr_dyn_obj_set(view, "generatedMessage", 16, scr_dyn_new_bool(generated)); + scr_error_set_prop(e, "generatedMessage", 16, scr_dyn_new_bool(generated)); scr_error_set_code(e, "ERR_ASSERTION"); - scr_dyn_obj_set(view, "code", 4, scr_assert_dyn_text("ERR_ASSERTION")); - scr_dyn_obj_set(view, "actual", 6, actual); - scr_dyn_obj_set(view, "expected", 8, expected); - scr_dyn_obj_set(view, "operator", 8, op); - scr_dyn_obj_set(view, "diff", 4, diff); - scr_dyn_release(view); + scr_error_set_prop(e, "code", 4, scr_assert_dyn_text("ERR_ASSERTION")); + scr_error_set_prop(e, "actual", 6, actual); + scr_error_set_prop(e, "expected", 8, expected); + scr_error_set_prop(e, "operator", 8, op); + scr_error_set_prop(e, "diff", 4, diff); } static ScrError *scr_assert_error(ScrStr *message, bool generated, const char *diff) { diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 95771d457..ffcb85050 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -1036,11 +1036,9 @@ void scr_process_emit_warning(ScrDyn *args) { 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); + if (detail) scr_error_set_prop(e, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ ScrDyn *w = scr_dyn_from_error(e); scr_error_release(e); - if (detail) { - scr_dyn_obj_set(w, "detail", 6, scr_dyn_new_str((ScrStr *)detail)); /* retains */ - } scr_warning_emit_deferred(w); scr_dyn_release(w); } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 1235001ba..87ead5755 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -62,6 +62,118 @@ enum { SCR_CHILD_DATA_STRING = 2, }; +/* ── error properties (lib/child_process.js) ────────────────────────── + * The own properties Node attaches to child_process errors, set on the + * error's checked-dynamic node (scr_error_set_prop) in Node's key order. */ + +static ScrDyn *scr_child_dyn_cstr(const char *text) { + ScrStr *s = scr_str_new(text, strlen(text)); + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; +} + +static ScrDyn *scr_child_dyn_text_or_null(const char *text) { + return text != NULL ? scr_child_dyn_cstr(text) : scr_dyn_new_null(); +} + +/* A spawn failure's errnoException fields: errno, code (already stamped), + * syscall " " and path come from the native system fields, + * then spawnargs (the arguments after the file). Set before any other + * property, so the node orders them first. */ +static void scr_child_error_spawn_fields(ScrError *e, double uv_errno, const char *verb, + ScrStr *file, ScrArr *args) { + size_t cap = strlen(verb) + 1 + file->len + 1; + char *call = malloc(cap); + if (!call) scr_trap("scriptc: out of memory\n"); + int len = snprintf(call, cap, "%s %s", verb, file->data); + e->system_errno = uv_errno; + e->system_call = scr_str_new(call, (size_t)len); + free(call); + e->system_path = scr_str_retain(file); + ScrDyn *spawnargs = scr_dyn_new_arr(); + size_t n = args != NULL ? (size_t)scr_arr_len(args) : 0; + for (size_t i = 0; i < n; i++) { + ScrStr *arg = (ScrStr *)scr_arr_get_ref(args, (double)i); + scr_dyn_arr_push(spawnargs, scr_dyn_new_str(arg)); + scr_str_release(arg); + } + scr_error_set_prop(e, "spawnargs", 9, spawnargs); +} + +/* exec/execFile's command failure: code (the exit status, or null after a + * signal), killed and signal, before cmd. */ +static void scr_child_error_exit_fields(ScrError *e, bool has_code, double code, + const char *signal, bool killed) { + scr_error_set_prop(e, "code", 4, has_code ? scr_dyn_new_num(code) : scr_dyn_new_null()); + scr_error_set_prop(e, "killed", 6, scr_dyn_new_bool(killed)); + scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal)); +} + +/* The sync forms' spawn failure and timeout carry the error itself as + * `error` (Node assigns the spawnSync result, whose error it is). */ +static void scr_child_error_self(ScrError *e) { + scr_error_set_prop(e, "error", 5, scr_dyn_from_error(e)); +} + +static void scr_child_error_cmd(ScrError *e, const char *cmd) { + scr_error_set_prop(e, "cmd", 3, scr_child_dyn_cstr(cmd)); +} + +/* The promisified forms add the captured outputs. Borrows. */ +static void scr_child_error_outputs(ScrError *e, ScrStr *out, ScrStr *err) { + scr_error_set_prop(e, "stdout", 6, scr_dyn_new_str(out)); + scr_error_set_prop(e, "stderr", 6, scr_dyn_new_str(err)); +} + +/* A captured sync output as Node's result holds it: a Buffer, the utf8 + * string with an encoding option, or null for a stream that was not + * piped. +1. */ +static ScrDyn *scr_child_sync_output(const char *data, size_t len, bool piped, bool buffers) { + if (!piped) return scr_dyn_new_null(); + if (!buffers) { + ScrStr *s = scr_str_new(data, len); + ScrDyn *d = scr_dyn_new_str(s); + scr_str_release(s); + return d; + } + ScrBytes *b = scr_bytes_new(SCR_BYTES_U8, (double)len); + b->is_buffer = true; + if (len) memcpy(b->data, data, len); + ScrDyn *d = scr_dyn_new_buffer(b); + scr_bytes_release(b); + return d; +} + +/* execSync/execFileSync: the spawnSync result's status, signal, output, + * pid, stdout and stderr. out/err move in (null for a stream that was not + * piped); output is [null, stdout, stderr]. A child that never started + * has a null output and undefined outputs. */ +static void scr_child_error_sync_fields(ScrError *e, bool has_status, double status, + const char *signal, double pid, bool started, + ScrDyn *out, ScrDyn *err) { + if (!started) { + scr_dyn_release(out); + scr_dyn_release(err); + out = scr_dyn_retain(scr_dyn_undefined()); + err = scr_dyn_retain(scr_dyn_undefined()); + } + scr_error_set_prop(e, "status", 6, has_status ? scr_dyn_new_num(status) : scr_dyn_new_null()); + scr_error_set_prop(e, "signal", 6, scr_child_dyn_text_or_null(signal)); + if (started) { + ScrDyn *output = scr_dyn_new_arr(); + scr_dyn_arr_push(output, scr_dyn_new_null()); + scr_dyn_arr_push(output, scr_dyn_retain(out)); + scr_dyn_arr_push(output, scr_dyn_retain(err)); + scr_error_set_prop(e, "output", 6, output); + } else { + scr_error_set_prop(e, "output", 6, scr_dyn_new_null()); + } + scr_error_set_prop(e, "pid", 3, scr_dyn_new_num(pid)); + scr_error_set_prop(e, "stdout", 6, out); + scr_error_set_prop(e, "stderr", 6, err); +} + #ifdef _WIN32 /* ── Windows arm: real children via CreateProcessW ──────────────────── * The POSIX arm below owns the corresponding implementation; this arm @@ -766,6 +878,7 @@ typedef struct { const char *spawn_errname; /* NULL = the child ran (and was reaped) */ bool timed_out; /* the deadline TerminateProcess fired */ DWORD exit_code; + DWORD pid; /* the child's process id (0 when it never ran) */ ScrCapBuf out, err; /* always initialized; caller frees */ } ScrWinSyncRes; @@ -780,6 +893,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input, res->spawn_errname = NULL; res->timed_out = false; res->exit_code = 0; + res->pid = 0; scr_cap_init(&res->out); scr_cap_init(&res->err); @@ -842,6 +956,7 @@ static void scr_win_run_sync(ScrStr *cmd, ScrArr *args, const ScrStr *input, res->spawn_errname = sp.errname; return; } + res->pid = sp.pid; /* Pump: drain both captures, feed stdin (PIPE_NOWAIT — a full pipe * writes short, never blocks), watch the deadline. The 1ms wait rides @@ -1082,8 +1197,8 @@ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) { return out; } -static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, - bool with_stderr, bool always_nl) { +static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap, + bool with_stderr, bool always_nl) { size_t dlen = strlen(display); size_t elen = with_stderr ? err_cap->len : 0; size_t cap = 16 + dlen + 1 + elen + 1; @@ -1095,18 +1210,33 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, memcpy(msg + at, err_cap->data, elen); at += elen; } - scr_throw_error_msg(SCR_ERR_ERROR, msg, at); + ScrStr *message = scr_str_new(msg, at); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + return e; } -static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) { +/* Node's spawn-failure/timeout error with its errnoException fields + * (errno is libuv's code for the name). +1. */ +static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, const char *errname, + bool async_shape) { + const char *verb = async_shape ? "spawn" : "spawnSync"; size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1; char *msg = malloc(cap); if (!msg) scr_child_oom(); - int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync", - cmd->data, errname); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname); + int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname); + ScrStr *message = scr_str_new(msg, (size_t)len); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(e, errname); + scr_child_error_spawn_fields(e, (double)scr_win_uv_errno(errname), verb, cmd, args); + return e; +} + +static void scr_exec_throw(ScrError *e /* moves */) { + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } /* The shared exec body (sync + promisified shapes — the POSIX arm's @@ -1115,7 +1245,7 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, const ScrStr *input, const ScrStr *cwd, ScrArr *env_pairs, double timeout_ms, int stdout_mode, int stderr_mode, bool async_shape, - ScrStr **stderr_out) { + bool buffers, ScrStr **stderr_out) { /* stdio "inherit"'s stdin rides bit 4 of stdout_mode (scr_runtime.h). */ bool stdin_inherit = (stdout_mode & 4) != 0; stdout_mode &= 3; @@ -1129,31 +1259,67 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, if (w.spawn_errname != NULL) { free(w.out.data); free(w.err.data); - scr_exec_throw_spawn(cmd, w.spawn_errname, async_shape); + ScrError *e = scr_exec_spawn_error(cmd, args, w.spawn_errname, async_shape); + if (async_shape) { + char *display = scr_exec_display(cmd, args, shell); + scr_child_error_cmd(e, display); + free(display); + ScrStr *empty = scr_str_new("", 0); + scr_child_error_outputs(e, empty, empty); + scr_str_release(empty); + } else { + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL); + } + scr_exec_throw(e); return NULL; } - if (w.timed_out && !async_shape) { - /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape - * reports the kill's exit-1 death as an ordinary command failure - * below, exactly Node's promisified execFile. */ - free(w.out.data); - free(w.err.data); - scr_exec_throw_spawn(cmd, "ETIMEDOUT", false); - return NULL; - } - bool failed = w.exit_code != 0; /* Node's inheritStderr: no stdio option given → the captured stderr - * echoes to the parent's stderr AFTER completion, success or failure. */ + * echoes to the parent's stderr AFTER completion, success, failure or + * timeout. */ if (stderr_mode == 0 && w.err.len > 0) { fflush(stdout); fwrite(w.err.data, 1, w.err.len, stderr); } + if (w.timed_out && !async_shape) { + /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape + * reports the kill's death as an ordinary command failure below, + * exactly Node's promisified execFile. */ + ScrError *e = scr_exec_spawn_error(cmd, args, "ETIMEDOUT", false); + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, "SIGTERM", (double)w.pid, true, + scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers), + scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers)); + free(w.out.data); + free(w.err.data); + scr_exec_throw(e); + return NULL; + } + bool failed = w.exit_code != 0 || w.timed_out; if (failed) { char *display = scr_exec_display(cmd, args, shell); - scr_exec_throw_failed(display, &w.err, cap_err, async_shape); + ScrError *e = scr_exec_failed_error(display, &w.err, cap_err, async_shape); + /* libuv's kill emulation: a timed-out child died to the kill signal. */ + bool exited = !w.timed_out; + if (async_shape) { + scr_child_error_exit_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM", + w.timed_out); + scr_child_error_cmd(e, display); + ScrStr *out_s = scr_str_new(w.out.data, w.out.len); + ScrStr *err_s = scr_str_new(w.err.data, w.err.len); + scr_child_error_outputs(e, out_s, err_s); + scr_str_release(out_s); + scr_str_release(err_s); + } else { + scr_child_error_sync_fields(e, exited, (double)w.exit_code, exited ? NULL : "SIGTERM", + (double)w.pid, true, + scr_child_sync_output(w.out.data, w.out.len, cap_out, buffers), + scr_child_sync_output(w.err.data, w.err.len, cap_err, buffers)); + } free(display); free(w.out.data); free(w.err.data); + scr_exec_throw(e); return NULL; } if (async_shape && stderr_out != NULL) { @@ -1167,10 +1333,11 @@ static ScrStr *scr_exec_run(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode) { + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers) { return scr_exec_run(cmd, args, shell, has_input ? input : NULL, cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL, - timeout_ms, (int)stdout_mode, (int)stderr_mode, false, NULL); + timeout_ms, (int)stdout_mode, (int)stderr_mode, false, buffers, NULL); } ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd, @@ -1178,7 +1345,7 @@ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd ScrStr *err_s = NULL; ScrStr *out_s = scr_exec_run(cmd, args, shell, NULL, cwd->len > 0 ? cwd : NULL, has_env ? env_pairs : NULL, timeout_ms, 1, 1, - true, &err_s); + true, false, &err_s); if (out_s == NULL) return NULL; /* exception pending; dummy result */ return scr_spawn_res_new(true, 0, out_s, err_s); } @@ -1302,6 +1469,9 @@ struct ScrChild { ScrClosure *exec_cb; ScrExecFileFn exec_fn; ScrStr *exec_display; + ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */ + ScrArr *exec_args; /* a spawn failure's path and spawnargs */ + void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */ ScrChildStream *out_stream; ScrChildStream *err_stream; ScrChildWriter *in_stream; @@ -1356,6 +1526,8 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_str_release(c->exec_file); + if (c->exec_args) scr_arr_release(c->exec_args); scr_dyn_release(c->out_dyn); scr_dyn_release(c->err_dyn); scr_dyn_release(c->in_dyn); @@ -2236,10 +2408,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode, return child; } +/* Node's properties on the callback form's error. Reached only through + * the hook scr_exec_file installs, so programs without the callback form + * link none of the property code (the settle path is shared by every + * child). */ +static void scr_exec_file_error_props(ScrChild *c, ScrError *error) { + if (c->state == SCR_CHILD_SPAWN_FAILED) { + scr_child_error_spawn_fields(error, (double)c->spawn_uv_errno, "spawn", c->exec_file, c->exec_args); + } else { + scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed); + } + scr_child_error_cmd(error, c->exec_display->data); +} + static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { if (c->state == SCR_CHILD_SPAWN_FAILED) { ScrError *error = scr_error_new(0, c->err_msg); scr_error_set_code(error, c->spawn_errname); + if (c->exec_props) c->exec_props(c, error); return error; } if (c->has_code && c->code == 0) return NULL; @@ -2254,6 +2440,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { free(msg); ScrError *error = scr_error_new(0, message); scr_str_release(message); + if (c->exec_props) c->exec_props(c, error); return error; } @@ -2273,6 +2460,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; + c->exec_file = scr_str_retain(cmd); + c->exec_args = scr_arr_retain(args); + c->exec_props = &scr_exec_file_error_props; char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); @@ -3099,9 +3289,10 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ * the child completes (Node's inheritStderr flag — the capture is * unconditional); an explicit stdio option turns that off, and an * "ignore" stderr slot discards it entirely (nothing in the message). - * - The thrown error carries the MESSAGE only: Node's status/stdout/ - * stderr properties don't exist here (SEMANTICS.md — nothing the - * supported catch forms can read anyway). + * - The thrown error carries Node's own properties: status, signal, + * output, pid, stdout and stderr (Buffers, or strings with an encoding + * option), after the errnoException fields on a spawn failure or a + * timeout. * The result is the captured utf8 stdout (+1). */ typedef struct { @@ -3121,6 +3312,8 @@ typedef struct { * lands and the death reports as an ordinary command failure, exactly * Node's promisified behavior. */ bool async_shape; + /* The sync forms' error outputs are Buffers (no encoding option). */ + bool buffers; /* async_shape success: the captured stderr string (+1) lands here. */ ScrStr **stderr_out; } ScrExecOpts; @@ -3187,12 +3380,12 @@ static void scr_exec_envp_free(char **envp) { free(envp); } -/* The Command-failed thrower (message only — see the block comment). +/* The Command-failed error (+1; the caller adds its properties). * always_nl is the async shape: Node's promisified execFile appends * "\n" unconditionally (a trailing newline on empty stderr), * where the sync form appends only when stderr is non-empty. */ -static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, - bool with_stderr, bool always_nl) { +static ScrError *scr_exec_failed_error(const char *display, const ScrCapBuf *err_cap, + bool with_stderr, bool always_nl) { size_t dlen = strlen(display); size_t elen = with_stderr ? err_cap->len : 0; size_t cap = 16 + dlen + 1 + elen + 1; @@ -3204,22 +3397,34 @@ static void scr_exec_throw_failed(const char *display, const ScrCapBuf *err_cap, memcpy(msg + at, err_cap->data, elen); at += elen; } - scr_throw_error_msg(SCR_ERR_ERROR, msg, at); + ScrStr *message = scr_str_new(msg, at); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + return e; } -/* "spawnSync " — Node's uv-error message for spawn - * failures and timeouts, with `code` = the errno name (Node's - * errnoException shape; the Command-failed error stays code-less — Node - * puts the exit STATUS there, in properties this surface doesn't carry). */ -static void scr_exec_throw_spawn(ScrStr *cmd, const char *errname, bool async_shape) { +/* "spawnSync " — Node's uv-error for spawn failures and + * timeouts (+1): `code` is the errno name, and the errnoException fields + * (errno, syscall, path, spawnargs) follow. */ +static ScrError *scr_exec_spawn_error(ScrStr *cmd, ScrArr *args, int errnum, bool async_shape) { + const char *errname = scr_errname(errnum); + const char *verb = async_shape ? "spawn" : "spawnSync"; size_t cap = 10 + cmd->len + 1 + strlen(errname) + 1; char *msg = malloc(cap); if (!msg) scr_child_oom(); - int len = snprintf(msg, cap, "%s %s %s", async_shape ? "spawn" : "spawnSync", - cmd->data, errname); - scr_throw_error_msg_code(SCR_ERR_ERROR, msg, (size_t)len, errname); + int len = snprintf(msg, cap, "%s %s %s", verb, cmd->data, errname); + ScrStr *message = scr_str_new(msg, (size_t)len); free(msg); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(e, errname); + scr_child_error_spawn_fields(e, -(double)errnum, verb, cmd, args); + return e; +} + +static void scr_exec_throw(ScrError *e /* moves */) { + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { @@ -3286,7 +3491,19 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { if (has_input) close(infd[1]); /* execSync's ENOENT is the SHELL missing — can't happen; the file * form reports Node's spawnSync error. */ - scr_exec_throw_spawn(cmd, scr_errname(spawn_err), o->async_shape); + ScrError *e = scr_exec_spawn_error(cmd, args, spawn_err, o->async_shape); + if (o->async_shape) { + char *display = scr_exec_display(cmd, args, o->shell); + scr_child_error_cmd(e, display); + free(display); + ScrStr *empty = scr_str_new("", 0); + scr_child_error_outputs(e, empty, empty); + scr_str_release(empty); + } else { + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, NULL, 0, false, NULL, NULL); + } + scr_exec_throw(e); return NULL; } @@ -3379,29 +3596,53 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { } while (reaped < 0 && errno == EINTR); } - char *display = NULL; - if (timed_out && !o->async_shape) { - /* The SYNC forms throw Node's spawnSync ETIMEDOUT; the async shape - * reports the SIGTERM death as an ordinary command failure below, - * exactly Node's promisified execFile. */ - free(out.data); - free(err.data); - scr_exec_throw_spawn(cmd, "ETIMEDOUT", false); - return NULL; - } - bool failed = !WIFEXITED(st) || WEXITSTATUS(st) != 0; /* Node's inheritStderr: no stdio option given → the captured stderr - * echoes to the parent's stderr AFTER completion, success or failure. */ + * echoes to the parent's stderr AFTER completion, success, failure or + * timeout. */ if (o->stderr_mode == 0 && err.len > 0) { fflush(stdout); fwrite(err.data, 1, err.len, stderr); } + bool exited = WIFEXITED(st) && !(timed_out && !o->async_shape); + const char *signal = WIFSIGNALED(st) ? scr_signal_name(WTERMSIG(st)) : NULL; + if (timed_out && !o->async_shape) { + /* The SYNC forms throw Node's spawnSync ETIMEDOUT (signal = the kill + * signal even when the child raced it); the async shape reports the + * SIGTERM death as an ordinary command failure below, exactly Node's + * promisified execFile. */ + ScrError *e = scr_exec_spawn_error(cmd, args, ETIMEDOUT, false); + scr_child_error_self(e); + scr_child_error_sync_fields(e, false, 0, signal != NULL ? signal : "SIGTERM", (double)pid, + true, scr_child_sync_output(out.data, out.len, cap_out, o->buffers), + scr_child_sync_output(err.data, err.len, cap_err, o->buffers)); + free(out.data); + free(err.data); + scr_exec_throw(e); + return NULL; + } + bool failed = !exited || WEXITSTATUS(st) != 0; if (failed) { - display = scr_exec_display(cmd, args, o->shell); - scr_exec_throw_failed(display, &err, cap_err, o->async_shape); + char *display = scr_exec_display(cmd, args, o->shell); + ScrError *e = scr_exec_failed_error(display, &err, cap_err, o->async_shape); + if (o->async_shape) { + scr_child_error_exit_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal, + timed_out); + scr_child_error_cmd(e, display); + ScrStr *out_s = scr_str_new(out.data, out.len); + ScrStr *err_s = scr_str_new(err.data, err.len); + scr_child_error_outputs(e, out_s, err_s); + scr_str_release(out_s); + scr_str_release(err_s); + } else { + scr_child_error_sync_fields(e, exited, exited ? (double)WEXITSTATUS(st) : 0, signal, + (double)pid, true, + scr_child_sync_output(out.data, out.len, cap_out, o->buffers), + scr_child_sync_output(err.data, err.len, cap_err, o->buffers)); + } free(display); free(out.data); free(err.data); + scr_exec_throw(e); return NULL; } if (o->async_shape && o->stderr_out != NULL) { @@ -3421,7 +3662,8 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) { * it means an empty environment). */ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode) { + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers) { ScrExecOpts o = { .shell = shell, .input = has_input ? input : NULL, @@ -3433,6 +3675,7 @@ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, .stdout_mode = (int)stdout_mode & 3, .stderr_mode = (int)stderr_mode, .stdin_inherit = ((int)stdout_mode & 4) != 0, + .buffers = buffers, }; return scr_exec_sync_core(cmd, args, &o); } @@ -3634,6 +3877,9 @@ struct ScrChild { ScrClosure *exec_cb; ScrExecFileFn exec_fn; ScrStr *exec_display; + ScrStr *exec_file; /* the spawned file and its arguments (+1 each), for */ + ScrArr *exec_args; /* a spawn failure's path and spawnargs */ + void (*exec_props)(struct ScrChild *c, ScrError *error); /* Node's error properties */ /* Piped stdio (stdio mode 3): the stream handles child.stdout / * child.stderr answer — owned (+1 each), NULL when not piped. */ ScrChildStream *out_stream; @@ -3828,6 +4074,8 @@ void scr_child_release(ScrChild *c) { scr_child_drop_listeners(c); /* only reachable pre-settle via leaks */ scr_str_release(c->err_msg); scr_str_release(c->exec_display); + scr_str_release(c->exec_file); + if (c->exec_args) scr_arr_release(c->exec_args); scr_dyn_release(c->out_dyn); scr_dyn_release(c->err_dyn); scr_dyn_release(c->in_dyn); @@ -3858,6 +4106,7 @@ static const char *scr_errname(int err) { case EAGAIN: return "EAGAIN"; case EIO: return "EIO"; case EINVAL: return "EINVAL"; + case ETIMEDOUT: return "ETIMEDOUT"; default: return "EUNKNOWN"; } } @@ -4881,10 +5130,24 @@ ScrChild *scr_fork(double target, ScrArr *args, double in_mode, return child; } +/* Node's properties on the callback form's error. Reached only through + * the hook scr_exec_file installs, so programs without the callback form + * link none of the property code (the settle path is shared by every + * child). */ +static void scr_exec_file_error_props(ScrChild *c, ScrError *error) { + if (c->state == SCR_CHILD_SPAWN_FAILED) { + scr_child_error_spawn_fields(error, -(double)c->spawn_errno, "spawn", c->exec_file, c->exec_args); + } else { + scr_child_error_exit_fields(error, c->has_code, c->code, c->exit_signal, c->killed); + } + scr_child_error_cmd(error, c->exec_display->data); +} + static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { if (c->state == SCR_CHILD_SPAWN_FAILED) { ScrError *error = scr_error_new(0, c->err_msg); scr_error_set_code(error, scr_errname(c->spawn_errno)); + if (c->exec_props) c->exec_props(c, error); return error; } if (c->has_code && c->code == 0) return NULL; @@ -4899,6 +5162,7 @@ static ScrError *scr_exec_file_error(ScrChild *c, ScrStr *stderr_value) { free(msg); ScrError *error = scr_error_new(0, message); scr_str_release(message); + if (c->exec_props) c->exec_props(c, error); return error; } @@ -4918,6 +5182,9 @@ ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb, scr_str_release(empty); c->exec_cb = cb; c->exec_fn = fn; + c->exec_file = scr_str_retain(cmd); + c->exec_args = scr_arr_retain(args); + c->exec_props = &scr_exec_file_error_props; char *display = scr_exec_display(cmd, args, shell); c->exec_display = scr_str_new(display, strlen(display)); free(display); diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h index 1819b9c53..1187985c8 100644 --- a/packages/runtime/src/scr_child.h +++ b/packages/runtime/src/scr_child.h @@ -44,18 +44,22 @@ ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ * 0 captures AND echoes to the parent stderr after completion (Node's * no-stdio-option default), 1 captures only, 2 ignores, 3 inherits. * Result is the captured utf8 stdout (+1) — "" when nothing captures - * (Node answers null there; discarded results cannot tell). All args + * (Node answers null there; discarded results cannot tell). A failure's + * error carries Node's status, signal, output, pid, stdout and stderr; + * `buffers` (no encoding option) makes those outputs Buffers. All args * borrowed. */ ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input, bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs, - double timeout_ms, double stdout_mode, double stderr_mode); + double timeout_ms, double stdout_mode, double stderr_mode, + bool buffers); /* The promisified-execFile/exec capture (cp.execCapture; shell as for * scr_exec_sync — the /bin/sh -c argv): the same core in the * async shape — both streams captured (no echo), Node's ASYNC error * messages on the throw paths ("Command failed: \n" with the * unconditional newline, "spawn " with .code, timeouts as - * ordinary SIGTERM command failures). +1 ScrSpawnRes on success (stdout + - * stderr; status unused). All args borrowed. */ + * ordinary SIGTERM command failures), each error with Node's code, killed, + * signal, cmd, stdout and stderr properties. +1 ScrSpawnRes on success + * (stdout + stderr; status unused). All args borrowed. */ ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *cwd, bool has_env, ScrArr *env_pairs, double timeout_ms); ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r); diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index 97b7a3e97..df62a79a3 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -159,22 +159,20 @@ static void sc_throw_openssl(const char *packed, const char *reason, const char snprintf(message, sizeof message, "error:%s:Provider routines::%s", packed, reason); ScrStr *text = scr_str_new(message, strlen(message)); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); - ScrDyn *view = scr_dyn_from_error(e); ScrDyn *stack = scr_dyn_new_arr(); scr_dyn_arr_push(stack, scr_dyn_new_str(text)); scr_str_release(text); - scr_dyn_obj_set(view, "opensslErrorStack", 17, stack); + scr_error_set_prop(e, "opensslErrorStack", 17, stack); ScrStr *library = scr_str_new("Provider routines", 17); - scr_dyn_obj_set(view, "library", 7, scr_dyn_new_str(library)); + scr_error_set_prop(e, "library", 7, scr_dyn_new_str(library)); scr_str_release(library); ScrStr *reason_text = scr_str_new(reason, strlen(reason)); - scr_dyn_obj_set(view, "reason", 6, scr_dyn_new_str(reason_text)); + scr_error_set_prop(e, "reason", 6, scr_dyn_new_str(reason_text)); scr_str_release(reason_text); scr_error_set_code(e, code); ScrStr *code_text = scr_str_new(code, strlen(code)); - scr_dyn_obj_set(view, "code", 4, scr_dyn_new_str(code_text)); + scr_error_set_prop(e, "code", 4, scr_dyn_new_str(code_text)); scr_str_release(code_text); - scr_dyn_release(view); scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } diff --git a/packages/runtime/src/scr_dgram.c b/packages/runtime/src/scr_dgram.c index b02c41c03..c34923607 100644 --- a/packages/runtime/src/scr_dgram.c +++ b/packages/runtime/src/scr_dgram.c @@ -1297,9 +1297,9 @@ static ScrDyn *scr_dns_exception(int rc, const char *syscall, const ScrStr *host scr_error_set_code(e, code); e->system_call = scr_str_new(syscall, strlen(syscall)); e->system_errno = errnum; + scr_error_set_prop(e, "hostname", 8, scr_dyn_new_str((ScrStr *)host)); ScrDyn *d = scr_dyn_from_error(e); scr_error_release(e); - scr_dyn_obj_set(d, "hostname", 8, scr_dyn_new_str((ScrStr *)host)); return d; } diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bccd8d55c..86fcc7709 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -998,6 +998,10 @@ ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */ * the gated extraction reads/writes through these). */ ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */ void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */ +/* Sets an own property on an error: its checked-dynamic node, which casts + * and util.inspect read. Moves value. */ +void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value); +ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */ /* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */ bool scr_dyn_truthy(const ScrDyn *d); /* JS String() over a dyn value (arrays join, [object Object], the error diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 34438353f..4aa703711 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -801,14 +801,32 @@ static ScrStr *scr_insp_array_buffer(const ScrBytes *h, const char *cls, double return out; } +static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth); + +/* A runtime error through its node: the cached one, or one built from its + * fields. */ +ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth) { + ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_OBJ) return scr_insp_dyn_error(view, recurse, depth); + ScrDyn *fields = scr_error_dyn_fields(e); + ScrStr *out = scr_insp_dyn_error(fields, recurse, depth); + scr_dyn_release(fields); + return out; +} + /* Errors: the STACKLESS rendering — compiled binaries carry no stack, so * the base is formatError's bracket form `[Name: message]` / `[Name]` * (Node prints these exact forms for an error whose stack is empty; - * stack-carrying Node output is a documented divergence). A stamped - * `code` slot renders as the one extra own property Node would show: - * `[Error: m] { code: 'X' }`, `[Error]` beyond depth. Message newlines - * indent like Node's formatError tail. */ + * stack-carrying Node output is a documented divergence). A runtime error + * renders through its checked-dynamic node, so the own properties Node + * shows follow in key order: the system-error fields, code, and the extra + * properties a throw site attached (`[Error: m] { errno: -2, code: + * 'ENOENT', ... }`). Other errors render a stamped `code` slot as their + * one extra property: `[Error: m] { code: 'X' }`, `[Error]` beyond depth. + * Message newlines indent like Node's formatError tail. */ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) { + if (e->vt >= &scr_error_vts[0] && e->vt < &scr_error_vts[SCR_ERR_COUNT]) + return scr_insp_error_node(e, recurse, depth); bool has_code = e->code != NULL; /* the stamped slot, read directly (borrowed) */ if (has_code && recurse > depth) { /* `[${getCtxStyle(...)}]` with the trailing space sliced: [Name] */ @@ -897,13 +915,53 @@ static const ScrDyn *insp_error_slot(const ScrDyn *d, const char *key, size_t le return v != NULL && v->kind == SCR_DYN_STR ? v : NULL; } +/* AssertionError's custom inspect (lib/internal/assert/assertion_error.js) + * renders the error at depth 0 and shortens string actual/expected values + * with addEllipsis: past 10 lines, the first 10 and "\n..."; otherwise past + * 512 characters, the text from offset 512 on and "...". +1. */ +static ScrDyn *insp_assertion_ellipsis(ScrDyn *value) { + if (value->kind != SCR_DYN_STR) return scr_dyn_retain(value); + ScrStr *text = value->v.str; + size_t lines = 1, cut = 0; + for (size_t i = 0; i < text->len; i++) { + if (text->data[i] != '\n') continue; + if (++lines == 11) { + cut = i; + break; + } + } + InspBuf b = {0}; + if (lines > 10) { + ib_bytes(&b, text->data, cut); + ib_cstr(&b, "\n..."); + } else if (scr_str_utf16_len(text) > 512) { + ScrStr *tail = scr_str_slice(text, 512, scr_str_utf16_len(text)); + ib_str(&b, tail); + ib_cstr(&b, "..."); + scr_str_release(tail); + } else { + return scr_dyn_retain(value); + } + ScrStr *shortened = ib_take(&b); + ScrDyn *out = scr_dyn_new_str(shortened); + scr_str_release(shortened); + return out; +} + /* The checked-dynamic Error encoding (scr_error_dyn_fields) rendered like * scr_insp_error: the stackless bracket form `[Name: message]`, then the * remaining enumerable own properties (code, errno, syscall, ...) in * Node's `{ ... }` tail. */ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { + /* An error already on the traversal stack (a spawnSync error's own + * `error`): Node's [Circular *N], and on the outer rendering. */ + double circular = scr_insp_circ_check(d); + if (circular != 0) return scr_insp_circular(circular); const ScrDyn *name = insp_error_slot(d, "name", 4); const ScrDyn *message = insp_error_slot(d, "message", 7); + bool assertion = name != NULL && name->v.str->len == 14 && + memcmp(name->v.str->data, "AssertionError", 14) == 0; + double value_depth = assertion ? recurse : depth; size_t extra = 0; for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; @@ -934,6 +992,7 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { ScrStr *base_str = ib_take(&base); if (extra == 0) return base_str; scr_insp_begin(recurse + 1); + scr_insp_seen_push(d); for (size_t i = 0; i < d->v.obj.len; i++) { ScrDynEntry *ent = &d->v.obj.entries[i]; if (!ent->enumerable) continue; @@ -941,7 +1000,11 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { (ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) || (ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0)) continue; - ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth); + bool shortened = assertion && ((ent->key_len == 6 && memcmp(ent->key, "actual", 6) == 0) || + (ent->key_len == 8 && memcmp(ent->key, "expected", 8) == 0)); + ScrDyn *shown = shortened ? insp_assertion_ellipsis(ent->value) : scr_dyn_retain(ent->value); + ScrStr *val = scr_insp_dyn(shown, recurse + 1, value_depth); + scr_dyn_release(shown); InspBuf eb = {0}; insp_key_into(&eb, ent->key, ent->key_len); ib_cstr(&eb, ": "); @@ -953,10 +1016,12 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) { } ScrStr *b0 = scr_str_new("{", 1); ScrStr *b1 = scr_str_new("}", 1); - ScrStr *out = scr_insp_end(base_str, b0, b1, recurse + 1, false, false); + ScrStr *body = scr_insp_end(base_str, b0, b1, recurse + 1, false, false); scr_str_release(b0); scr_str_release(b1); scr_str_release(base_str); + ScrStr *out = scr_insp_ref_wrap(d, body); + scr_str_release(body); return out; } diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index f8d28b4f0..4414330b5 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -26,10 +26,15 @@ ScrStr *scr_insp_regex(ScrRegex *re); /* /source/flags */ /* A Date (its time value): toISOString(), or "Invalid Date" for NaN (+1). */ ScrStr *scr_insp_date(double ms); ScrStr *scr_insp_buffer(ScrBytes *b); /* , 50-byte cap */ -/* The stackless error form: [Name: message] / [Name], plus the stamped - * code slot as its one extra property ({ code: 'X' }); [Name] beyond - * depth when a code makes it composite. */ +/* The stackless error form: [Name: message] / [Name], then a runtime + * error's own properties through its checked-dynamic node, or another + * error's stamped code slot as its one extra property ({ code: 'X' }); + * [Name] beyond depth when a code makes it composite. */ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth); +/* A runtime error rendered through its checked-dynamic node (the cached + * one, or one built from its fields), so Node's own properties follow the + * bracket form. */ +ScrStr *scr_insp_error_node(ScrError *e, double recurse, double depth); /* The checked-dynamic tree: shape lives in the value, so the whole * traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented * Uint8Array identity. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 401933904..0376ece2c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2568,10 +2568,26 @@ static bool scr_record_source_declared(const ScrRecordSource *source, const char return scr_record_source_index(source, key, key_len) < source->count; } +/* Frees the table at thread exit (a worker's table dies with its thread): + * the storage is detached before any remaining view is released. */ +static void scr_record_source_teardown(void) { + ScrRecordSource *table = scr_record_sources; + size_t capacity = scr_record_source_capacity; + scr_record_sources = NULL; + scr_record_source_capacity = scr_record_source_count = scr_record_source_deleted = 0; + free(scr_record_source_filter); + scr_record_source_filter = NULL; + for (size_t i = 0; i < capacity; i++) { + if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) scr_dyn_release(table[i].view); + } + free(table); +} + /* Inserts the entry of a record the caller just built; the table takes * the entry's view. A fresh record has no entry: the previous owner of its * address left the table when it was freed (the dispose hook). */ static void scr_record_source_put(const ScrRecordSource *entry) { + if (!scr_record_sources) scr_atexit(scr_record_source_teardown); scr_record_source_reserve(); if (!scr_record_source_filter) { scr_record_source_filter = calloc(((size_t)1 << SCR_RECORD_SOURCE_FILTER_BITS) / 64, @@ -6392,10 +6408,13 @@ ScrStr *scr_dyn_format_j(const ScrDyn *d) { * IDENTITY-CACHED: one error instance boxes to ONE dyn node, however many * times it crosses (Node passes the error OBJECT through, so `found.error * === thrown` and re-crossings compare reference-equal — the tracing - * suite's shape). The cache retains both sides for the process (like the - * dc registry; released atexit before the RC audit) and the node - * SNAPSHOTS name/message/code at first crossing — a later mutation of the - * error is invisible through it (SEMANTICS.md). Two pointer-keyed hash + * suite's shape). The cache retains both sides; an entry lives while + * either side is referenced elsewhere, and a sweep releases the entries + * whose error and node only the cache still references (the rest are + * released atexit before the RC audit). The node also holds the error's + * extra own properties (scr_error_set_prop) and SNAPSHOTS name/message/ + * code at first crossing — a later mutation of the error is invisible + * through it (SEMANTICS.md). Two pointer-keyed hash * indexes find an entry by either side: Promise.allSettled and * Promise.any cross every rejection reason. Both sides are unique per * entry (an error boxes once; an alien view enters once). */ @@ -6410,6 +6429,9 @@ static SCR_TL bool scr_errdyn_teardown_registered = false; /* Open-addressed indexes holding entry index + 1 (0 = empty slot). */ static SCR_TL size_t *scr_errdyn_by_err = NULL, *scr_errdyn_by_dyn = NULL; static SCR_TL size_t scr_errdyn_slots = 0; /* a power of two */ +/* The entry count that triggers the next sweep: twice the entries the + * previous sweep kept, so sweeping costs amortized O(1) per entry. */ +static SCR_TL size_t scr_errdyn_sweep_at = 64; static size_t scr_errdyn_hash(const void *p) { size_t x = (size_t)(uintptr_t)p >> 4; @@ -6445,8 +6467,70 @@ static size_t scr_errdyn_find(const void *key, bool by_err) { static void scr_errdyn_teardown(void); +/* The entries of a node that hold the node itself (a spawnSync error's + * `error`): references the node makes to itself, not reachability. */ +static size_t scr_errdyn_self_refs(const ScrDyn *d) { + if (d->kind != SCR_DYN_OBJ) return 0; + size_t n = 0; + for (size_t i = 0; i < d->v.obj.len; i++) n += d->v.obj.entries[i].value == d; + return n; +} + +/* Releases a node the cache owned, dropping its references to itself + * first so the release frees it. */ +static void scr_errdyn_release_node(ScrDyn *d) { + for (size_t i = 0; d->kind == SCR_DYN_OBJ && i < d->v.obj.len; i++) { + ScrDynEntry *entry = &d->v.obj.entries[i]; + if (entry->value != d) continue; + entry->value = scr_dyn_retain(scr_dyn_undefined()); + scr_dyn_release(d); + } + scr_dyn_release(d); +} + +/* Releases the entries nothing but the cache references. A field node + * holds no reference to its error and a registered subclass box holds + * one, so an entry whose node has only the cache's reference and whose + * error has no reference beyond the cache's and its box's is + * unreachable. The indexes are rebuilt over the + * kept entries before anything is released (a teardown never sees a + * stale entry); an entry a released node kept alive goes in a later + * sweep. */ +static void scr_errdyn_sweep(void) { + ScrErrDynEnt *dead = NULL; + size_t n_dead = 0, kept = 0; + for (size_t i = 0; i < scr_errdyn_n; i++) { + ScrErrDynEnt ent = scr_errdyn_cache[i]; + size_t boxed = ent.dyn->kind == SCR_DYN_TYPED_REF && ent.dyn->v.typed_ref.ptr == (void *)ent.err; + if (ent.dyn->rc == 1 + scr_errdyn_self_refs(ent.dyn) && ent.err->rc == 1 + boxed) { + if (dead == NULL) { + dead = malloc((scr_errdyn_n - i) * sizeof *dead); + if (!dead) scr_trap("scriptc: out of memory\n"); + } + dead[n_dead++] = ent; + } else { + scr_errdyn_cache[kept++] = ent; + } + } + scr_errdyn_n = kept; + scr_errdyn_sweep_at = kept * 2 > 64 ? kept * 2 : 64; + if (n_dead == 0) return; + memset(scr_errdyn_by_err, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_err); + memset(scr_errdyn_by_dyn, 0, scr_errdyn_slots * sizeof *scr_errdyn_by_dyn); + for (size_t i = 0; i < scr_errdyn_n; i++) { + scr_errdyn_index_put(scr_errdyn_by_err, scr_errdyn_cache[i].err, i, true); + scr_errdyn_index_put(scr_errdyn_by_dyn, scr_errdyn_cache[i].dyn, i, false); + } + for (size_t i = 0; i < n_dead; i++) { + scr_error_release(dead[i].err); + scr_errdyn_release_node(dead[i].dyn); + } + free(dead); +} + /* Appends an entry (both sides retained) and indexes it. */ static void scr_errdyn_append(ScrError *e, ScrDyn *d) { + if (scr_errdyn_n >= scr_errdyn_sweep_at) scr_errdyn_sweep(); 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); @@ -6502,17 +6586,21 @@ void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)) { } static void scr_errdyn_teardown(void) { - for (size_t i = 0; i < scr_errdyn_n; i++) { - scr_error_release(scr_errdyn_cache[i].err); - scr_dyn_release(scr_errdyn_cache[i].dyn); - } - free(scr_errdyn_cache); + /* Detach the storage first: no release below sees a half-torn cache. */ + ScrErrDynEnt *cache = scr_errdyn_cache; + size_t n = scr_errdyn_n; scr_errdyn_cache = NULL; scr_errdyn_n = scr_errdyn_cap = 0; free(scr_errdyn_by_err); free(scr_errdyn_by_dyn); scr_errdyn_by_err = scr_errdyn_by_dyn = NULL; scr_errdyn_slots = 0; + scr_errdyn_sweep_at = 64; + for (size_t i = 0; i < n; i++) { + scr_error_release(cache[i].err); + scr_errdyn_release_node(cache[i].dyn); + } + free(cache); } ScrDyn *scr_error_dyn_fields(const ScrError *e) { @@ -6540,7 +6628,11 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { 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->stack) { + /* Node's own `stack` is non-enumerable. */ + scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); + d->v.obj.entries[d->v.obj.len - 1].enumerable = false; + } if (e->system_message) { /* SystemError: code, the read-only `info` context ({ message, path, * syscall, errno, code }), then errno, syscall, path and dest. */ @@ -6617,6 +6709,23 @@ ScrDyn *scr_dyn_from_error(const ScrError *e) { return d; } +/* An own property on an error, as a throw site attaches Node's extra + * fields (a dns error's hostname, an exec error's status): set on the + * error's checked-dynamic node, which casts, the checked-dynamic surface + * and util.inspect read. Moves value. */ +void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *value) { + ScrDyn *view = scr_dyn_from_error(e); + scr_dyn_obj_set(view, key, key_len, value); + scr_dyn_release(view); +} + +/* The error's checked-dynamic node when it has one (borrowed), else NULL: + * util.inspect reads its extra own properties. */ +ScrDyn *scr_errdyn_view_of(const ScrError *e) { + size_t hit = scr_errdyn_find(e, true); + return hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].dyn; +} + /* The %Error EXTRACTION (dynCheck of `u as Error` / an instanceof-Error * narrow, and the dyn-boxed thunk's Error-typed parameters): the REVERSE * of scr_dyn_from_error, riding the same identity cache — a dyn error @@ -7741,7 +7850,15 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer)); return; } + bool created = scr_dyn_entry(recv, key) == NULL; scr_dyn_obj_set(recv, key->data, key->len, scr_dyn_retain(value)); + if (created && key->len == 5 && memcmp(key->data, "stack", 5) == 0 && + scr_errdyn_find(recv, false) != SIZE_MAX) { + /* An error's own `stack` exists from construction as a non-enumerable + * data property, so assigning it adds no enumerable key. */ + ScrDynEntry *entry = scr_dyn_entry(recv, key); + if (entry) entry->enumerable = false; + } scr_error_sync_cause(recv, key); return; } diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..6493494f6 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -8738,12 +8738,10 @@ static void scr_crypto_not_xof(void) { ScrStr *text = scr_str_new(message, sizeof message - 1); ScrError *e = scr_error_new(SCR_ERR_ERROR, text); scr_str_release(text); - ScrDyn *view = scr_dyn_from_error(e); - scr_dyn_obj_set(view, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25))); - scr_dyn_obj_set(view, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25))); + scr_error_set_prop(e, "library", 7, scr_crypto_dyn_str(scr_str_new("digital envelope routines", 25))); + scr_error_set_prop(e, "reason", 6, scr_crypto_dyn_str(scr_str_new("not XOF or invalid length", 25))); scr_error_set_code(e, code); - scr_dyn_obj_set(view, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1))); - scr_dyn_release(view); + scr_error_set_prop(e, "code", 4, scr_crypto_dyn_str(scr_str_new(code, sizeof code - 1))); scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); } diff --git a/packages/runtime/src/scr_util_callable.c b/packages/runtime/src/scr_util_callable.c index f5ef488dc..c905ea8ae 100644 --- a/packages/runtime/src/scr_util_callable.c +++ b/packages/runtime/src/scr_util_callable.c @@ -395,9 +395,9 @@ static ScrDyn *uc_callbackify_rejected(ScrClosure *closure, ScrDyn *const *args, ScrError *e = scr_error_new(SCR_ERR_ERROR, text); scr_str_release(text); scr_error_set_code(e, "ERR_FALSY_VALUE_REJECTION"); + scr_error_set_prop(e, "reason", 6, scr_dyn_retain(reason)); err = scr_dyn_from_error(e); scr_error_release(e); - scr_dyn_obj_set(err, "reason", 6, scr_dyn_retain(reason)); } uc_schedule_callback(cb, err, scr_dyn_retain(scr_dyn_undefined()), 1); scr_dyn_release(cb); diff --git a/tests/corpus/5634-error-extra-props-inspect.ts b/tests/corpus/5634-error-extra-props-inspect.ts new file mode 100644 index 000000000..b5d16ef97 --- /dev/null +++ b/tests/corpus/5634-error-extra-props-inspect.ts @@ -0,0 +1,64 @@ +// util.inspect of errors that carry Node's extra own properties: the +// AssertionError fields (rendered at depth 0, with long string values +// shortened), the falsy-rejection reason util.callbackify attaches, an +// OpenSSL-style crypto error's library and reason, and a system error's +// errno, syscall and path. Each error's stack is cleared first, so Node +// prints the same stackless form. +import * as assert from "node:assert"; +import * as crypto from "node:crypto"; +import * as fs from "node:fs"; +import { callbackify, inspect } from "node:util"; + +function show(e: unknown): void { + if (e instanceof Error) { + e.stack = ""; + console.log(inspect(e)); + } +} + +try { + assert.strictEqual(1, 2); +} catch (e) { + show(e); +} +type Doc = { a: number; nested?: { b: number[] } }; +const left: Doc = { a: 1, nested: { b: [1, 2] } }; +const right: Doc = { a: 2 }; +try { + assert.deepStrictEqual(left, right, "custom message"); +} catch (e) { + show(e); +} +try { + assert.notStrictEqual("x".repeat(600), "x".repeat(600)); +} catch (e) { + show(e); +} +try { + assert.ok(false); +} catch (e) { + show(e); +} +try { + throw new RangeError("no extra properties"); +} catch (e) { + show(e); +} +try { + crypto.createHash("sha256", { outputLength: 5 }); +} catch (e) { + show(e); +} +try { + fs.readFileSync("/no/such/dir/file.txt"); +} catch (e) { + show(e); + console.log(inspect({ error: e }, { depth: 0 })); +} + +async function rejectsNull(): Promise { + return Promise.reject(null); +} +callbackify(rejectsNull)((err: NodeJS.ErrnoException | null) => { + show(err); +}); diff --git a/tests/corpus/5635-error-extra-props-retry-loop.ts b/tests/corpus/5635-error-extra-props-retry-loop.ts new file mode 100644 index 000000000..8cba3b984 --- /dev/null +++ b/tests/corpus/5635-error-extra-props-retry-loop.ts @@ -0,0 +1,31 @@ +// Errors that cross into checked-dynamic code (a cast of a caught error) +// in a retry loop, on the main thread and in a worker at the same time: +// every crossing reads the properties of its own error. +import * as assert from "node:assert"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function round(n: number, offset: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + try { + assert.strictEqual(i + offset, -1); + } catch (e) { + const err = e as { code?: string; operator?: string; actual?: number }; + total += (err.code ?? "").length + (err.operator ?? "").length + (err.actual === i + offset ? 1 : 0); + } + } + return total; +} + +if (isMainThread) { + const worker = new Worker(new URL(import.meta.url)); + const fromWorker = new Promise((resolve) => worker.once("message", resolve)); + let sum = 0; + for (let r = 0; r < 5; r++) sum += round(5000, r); + console.log("main", sum); + console.log("worker", await fromWorker); +} else { + let sum = 0; + for (let r = 0; r < 5; r++) sum += round(5000, 100 + r); + parentPort?.postMessage(sum); +} diff --git a/tests/corpus/5636-child-error-props.ts b/tests/corpus/5636-child-error-props.ts new file mode 100644 index 000000000..b62f4385c --- /dev/null +++ b/tests/corpus/5636-child-error-props.ts @@ -0,0 +1,95 @@ +// child_process errors carry Node's own properties: the sync forms' +// status, signal, output, pid, stdout and stderr (Buffers, or strings with +// an encoding option), the async forms' code, killed, signal and cmd (with +// stdout and stderr when promisified), and a spawn failure's errno, +// syscall, path and spawnargs. Pids vary, so only their type prints. +import { exec, execFile, execFileSync, execSync } from "node:child_process"; +import { inspect, promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +function keys(e: unknown): string { + return Object.keys(e as object).join(","); +} + +function show(label: string, e: unknown): void { + if (!(e instanceof Error)) return; + const value: unknown = e; + const pid = typeof (value as { pid?: number }).pid; + e.stack = ""; + const text = inspect(e).replace(/pid: \d+/, "pid: "); + console.log(label, pid, keys(e)); + console.log(text); +} + +try { + execFileSync("/bin/sh", ["-c", "echo out; echo err 1>&2; exit 3"], { stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; signal?: string | null; stdout?: Buffer; stderr?: Buffer }; + console.log("status", err.status, err.signal, err.stdout?.toString(), err.stderr?.toString()); + show("execFileSync", e); +} +try { + execSync("echo text; exit 4", { encoding: "utf8", stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; stdout?: string; output?: (string | null)[] }; + console.log("encoding", err.status, JSON.stringify(err.stdout), inspect(err.output)); +} +try { + execSync("exit 5", { stdio: "ignore" }); +} catch (e) { + const err = e as { output?: unknown[]; stdout?: unknown }; + console.log("ignored", inspect(err.output), err.stdout); +} +try { + execFileSync("/bin/sh", ["-c", "kill -TERM $$"], { stdio: "pipe" }); +} catch (e) { + const err = e as { status?: number | null; signal?: string | null }; + console.log("signal", err.status, err.signal); +} +try { + execFileSync("/no/such/binary", ["a", "b"]); +} catch (e) { + show("spawn failure", e); + const err = e as { errno?: number; syscall?: string; path?: string; spawnargs?: string[] }; + console.log(err.errno, err.syscall, err.path, inspect(err.spawnargs)); +} +try { + execFileSync("/bin/sh", ["-c", "echo partial; exec sleep 5"], { timeout: 100, stdio: "pipe" }); +} catch (e) { + const err = e as { code?: string; status?: number | null; signal?: string | null; stdout?: Buffer }; + console.log("timeout", err.code, err.status, err.signal, err.stdout?.toString()); +} + +execFile("/bin/sh", ["-c", "echo o; exit 6"], (err, stdout) => { + const fields: unknown = err; + const e = fields as { code?: number | null; killed?: boolean; signal?: string | null; cmd?: string }; + console.log("callback", e.code, e.killed, e.signal, e.cmd, JSON.stringify(stdout)); + show("callback error", err); + execFile("/no/such/binary", ["x"], (missing) => { + show("callback spawn failure", missing); + exec("echo shell; exit 4", (shellError) => { + show("exec callback", shellError); + void promised(); + }); + }); +}); + +async function promised(): Promise { + try { + await execFileAsync("/bin/sh", ["-c", "echo po; echo pe 1>&2; exit 7"]); + } catch (e) { + show("promisified", e); + } + try { + await execFileAsync("/bin/sh", ["-c", "exec sleep 5"], { timeout: 100 }); + } catch (e) { + const err = e as { code?: number | null; killed?: boolean; signal?: string | null }; + console.log("promisified timeout", err.code, err.killed, err.signal); + } + try { + await execFileAsync("/no/such/binary"); + } catch (e) { + show("promisified spawn failure", e); + } +} diff --git a/tests/fixtures/error-cache-sweep/retry-loop.ts b/tests/fixtures/error-cache-sweep/retry-loop.ts new file mode 100644 index 000000000..3c58b0ba6 --- /dev/null +++ b/tests/fixtures/error-cache-sweep/retry-loop.ts @@ -0,0 +1,34 @@ +// 100,000 caught AssertionErrors read through a cast, on the main thread +// or (with the "worker" argument) in a worker; prints the resident-set +// growth in MiB after a warm-up round. +import * as assert from "node:assert"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; + +function round(n: number): number { + let total = 0; + for (let i = 0; i < n; i++) { + try { + assert.strictEqual(i, -1); + } catch (e) { + const err = e as { code?: string; operator?: string }; + total += (err.code ?? "").length + (err.operator ?? "").length; + } + } + return total; +} + +function measure(): number { + round(20000); + const before = process.memoryUsage().rss; + for (let r = 0; r < 5; r++) round(20000); + return Math.round((process.memoryUsage().rss - before) / 1048576); +} + +if (!isMainThread) { + parentPort?.postMessage(measure()); +} else if (process.argv[2] === "worker") { + const worker = new Worker(new URL(import.meta.url)); + worker.once("message", (growth: number) => console.log(growth)); +} else { + console.log(measure()); +} diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 000efcdfc..1a4ad1180 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1982,6 +1982,9 @@ "5631-promisify-crypto.ts": 2200, "5632-promisify-dns.ts": 1700, "5633-zlib-typed-options.ts": 1800, + "5634-error-extra-props-inspect.ts": 2300, + "5635-error-extra-props-retry-loop.ts": 3300, + "5636-child-error-props.ts": 7600, "5644-net-socket-addresses.ts": 2220, "5645-net-socket-callbacks.ts": 2442, "5646-net-server-connections.ts": 2389, @@ -5052,6 +5055,9 @@ "5631-promisify-crypto.ts": 2600, "5632-promisify-dns.ts": 2000, "5633-zlib-typed-options.ts": 2200, + "5634-error-extra-props-inspect.ts": 2000, + "5635-error-extra-props-retry-loop.ts": 6100, + "5636-child-error-props.ts": 9800, "5644-net-socket-addresses.ts": 2393, "5645-net-socket-callbacks.ts": 2060, "5646-net-server-connections.ts": 2755, diff --git a/tests/harness/error-cache-sweep.test.ts b/tests/harness/error-cache-sweep.test.ts new file mode 100644 index 000000000..821a0aae2 --- /dev/null +++ b/tests/harness/error-cache-sweep.test.ts @@ -0,0 +1,38 @@ +/* Errors that cross into checked-dynamic code stay in the runtime's error + * identity cache only while something references them: a retry loop that + * reads 100,000 caught errors through a cast keeps the resident set flat, + * on the main thread and in a worker (each thread sweeps its own cache). + * Plain lane only: the sanitizer's allocator quarantine holds freed + * memory, so resident size says nothing there. */ +import { execFile } from "node:child_process"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests/error-cache-sweep"); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +describe.skipIf(sanitize)("error identity cache", () => { + test("a retry loop of crossing errors keeps memory flat", { timeout: 300_000 }, async () => { + mkdirSync(outDir, { recursive: true }); + const result = await compile(join(repoRoot, "tests/fixtures/error-cache-sweep/retry-loop.ts"), { + outPath: join(outDir, "retry-loop"), + outDir, + backend: "llvm", + }); + if (!result.ok) { + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + } + for (const args of [[], ["worker"]]) { + const run = await execFileAsync(result.binaryPath, args); + expect(run.stderr).toBe(""); + expect(Number(run.stdout.trim()), `growth in MiB (${args.join(" ") || "main"})`).toBeLessThan( + 16, + ); + } + }); +}); From b6e9e42308d9c2565ee02c0cd4072838389e0fe2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 21:56:43 -0500 Subject: [PATCH 46/67] Derive the docs compatibility metadata at build time node-v24-compatibility-meta.json held two values derived from the public compatibility artifact: a content hash for the data URL and its row count. Every compatibility change rewrote the hash line, so concurrent changes conflicted on it. next.config.mjs now computes both from the committed artifact and passes them to the page through Next's build-time env, with the same values. The generator no longer writes the file, and its local check compares the public artifact itself. --- AGENTS.md | 1 - docs/next.config.mjs | 11 +++++++++ .../components/node-compatibility-matrix.tsx | 13 +++++++---- .../node-v24-compatibility-meta.json | 7 ------ docs/test/routes.test.mjs | 7 ++++-- internal/compatibility/src/generate.mjs | 23 ++----------------- 6 files changed, 26 insertions(+), 36 deletions(-) delete mode 100644 docs/src/generated/node-v24-compatibility-meta.json diff --git a/AGENTS.md b/AGENTS.md index b94c90067..c2bdbea2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,7 +140,6 @@ Never hand-edit these files: - `internal/compatibility/generated/node-v24-internal.json` - `internal/compatibility/generated/node-v24-backlog.json` - `docs/src/generated/node-v24-compatibility.json` -- `docs/src/generated/node-v24-compatibility-meta.json` Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes. diff --git a/docs/next.config.mjs b/docs/next.config.mjs index 36c0ea2cb..7079a6844 100644 --- a/docs/next.config.mjs +++ b/docs/next.config.mjs @@ -1,10 +1,21 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; import { createGeistdocs } from "@vercel/geistdocs/next"; const withGeistdocs = createGeistdocs(); const legacySlugs = ["quickstart", "coverage", "dependencies", "ffi", "native-objects", "wasm", "platforms", "cli", "how-it-works", "limitations"]; +// The compatibility page fetches its data under a content hash of the +// committed artifact, so a deploy never serves a cached older copy, and +// checks the row count it expects. Both are derived here at build time. +const compatibilityArtifact = readFileSync(new URL("./src/generated/node-v24-compatibility.json", import.meta.url), "utf8"); + /** @type {import('next').NextConfig} */ const nextConfig = { + env: { + COMPATIBILITY_ARTIFACT_VERSION: createHash("sha256").update(compatibilityArtifact).digest("hex").slice(0, 20), + COMPATIBILITY_ROW_COUNT: String(JSON.parse(compatibilityArtifact).rows.length), + }, agentRules: false, allowedDevOrigins: ["127.0.0.1"], distDir: process.env.NEXT_DIST_DIR || ".next", diff --git a/docs/src/components/node-compatibility-matrix.tsx b/docs/src/components/node-compatibility-matrix.tsx index f0a817a2f..095bcdaf2 100644 --- a/docs/src/components/node-compatibility-matrix.tsx +++ b/docs/src/components/node-compatibility-matrix.tsx @@ -5,7 +5,10 @@ // @refresh reset import { useEffect, useMemo, useState } from "react"; -import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json"; + +// Set by next.config.mjs from the committed artifact. +const artifactVersion = process.env.COMPATIBILITY_ARTIFACT_VERSION; +const rowCount = Number(process.env.COMPATIBILITY_ROW_COUNT); type Status = | "supported" @@ -211,14 +214,14 @@ export function NodeCompatibilityMatrix() { useEffect(() => { const controller = new AbortController(); - fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal }) + fetch(`/compatibility/data?v=${artifactVersion}`, { cache: "no-store", signal: controller.signal }) .then((response) => { if (!response.ok) throw new Error(`compatibility data: ${response.status}`); return response.json() as Promise; }) .then((data) => { - if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) { - throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`); + if (!Array.isArray(data.rows) || data.rows.length !== rowCount) { + throw new Error(`compatibility data is stale or malformed (expected ${rowCount} rows)`); } setSnapshot(data); }) @@ -281,7 +284,7 @@ export function NodeCompatibilityMatrix() { return
The compatibility dataset could not be loaded.
; } if (!snapshot) { - return
Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…
; + return
Loading {rowCount.toLocaleString()} Node.js API rows…
; } return ( diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json deleted file mode 100644 index 1cf76d3e4..000000000 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schemaVersion": 3, - "nodeVersion": "24.22.0", - "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "efc18bcd8a7a03fd8622", - "rowCount": 3880 -} diff --git a/docs/test/routes.test.mjs b/docs/test/routes.test.mjs index bd4b27bde..1520a5aa3 100644 --- a/docs/test/routes.test.mjs +++ b/docs/test/routes.test.mjs @@ -151,8 +151,11 @@ test("compatibility page and data endpoints keep their existing contracts", asyn const response = await get("/compatibility/data"); assert.equal(response.status, 200); const dataset = await response.json(); - const metadata = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility-meta.json", import.meta.url), "utf8")); - assert.equal(dataset.rows.length, metadata.rowCount); + const artifact = JSON.parse(await readFile(new URL("../src/generated/node-v24-compatibility.json", import.meta.url), "utf8")); + assert.equal(dataset.rows.length, artifact.rows.length); + // The row count next.config.mjs derives reaches the page (React separates + // adjacent text nodes with empty comments). + assert.ok(html.replaceAll("", "").includes(`Loading ${artifact.rows.length.toLocaleString()} Node.js API rows`)); assert.equal((await get("/node-compatibility")).status, 308); assert.equal((await get("/node-compatibility/data")).status, 308); }); diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 3433e14f9..8c7f136cf 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -24,7 +24,6 @@ const root = fileURLToPath(new URL("../../../", import.meta.url)); const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`; const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`; const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`; -const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`; const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`; const compilerCompatPath = `${packageRoot}static-support.json`; const pinPath = `${packageRoot}node-v24.json`; @@ -1009,17 +1008,6 @@ function publicSnapshot(snapshot) { }; } -function publicMetadata(publicRendered, snapshot) { - const matrix = projectPublicMatrix(snapshot); - return { - schemaVersion: 3, - nodeVersion: snapshot.nodeVersion, - nodeCommit: snapshot.nodeCommit, - artifactVersion: sha(publicRendered).slice(0, 20), - rowCount: matrix.rows.length, - }; -} - function checkLocal() { const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8")); if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) { @@ -1071,11 +1059,8 @@ function checkLocal() { if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) { throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'"); } - const publicRendered = render(publicSnapshot(snapshot)); - if ( - readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot)) - ) { - throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'"); + if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) { + throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'"); } for (const row of snapshot.rows) { for (const key of ["static"]) { @@ -1141,7 +1126,6 @@ async function main() { const internalRendered = render(snapshot); const backlogRendered = render(buildBacklog(snapshot)); const publicRendered = render(publicSnapshot(snapshot)); - const publicMetaRendered = render(publicMetadata(publicRendered, snapshot)); if (args.has("--check")) { if (readFileSync(internalOutputPath, "utf8") !== internalRendered) @@ -1150,8 +1134,6 @@ async function main() { throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'"); if (readFileSync(publicOutputPath, "utf8") !== publicRendered) throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'"); - if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered) - throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'"); console.log( `Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`, ); @@ -1160,7 +1142,6 @@ async function main() { writeFileSync(internalOutputPath, internalRendered); writeFileSync(backlogOutputPath, backlogRendered); writeFileSync(publicOutputPath, publicRendered); - writeFileSync(publicMetaOutputPath, publicMetaRendered); console.log( `wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`, ); From b9cc0ed82027d27f1b00b8af26635e54b26960f0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:06:48 -0500 Subject: [PATCH 47/67] Sort the handle tags by name with explicit values Every change that added a checked-dynamic handle kind appended its tag just before SCR_DYNH_COUNT in scr_dyn.h, so any two such changes in flight conflicted there. The tags are now sorted by name, each with its current value written out, so a new tag goes in its sorted place with the next free value, and changes adding different tags edit different lines. SCR_DYNH_COUNT, used only by the capacity check, is gone. The values are unchanged and the runtime artifacts are identical. A new test checks what the enum's order used to guarantee: values are unique and below SCR_DYNH_CAPACITY, and the LLVM backend's hardcoded tag numbers (DYN_HANDLE_TAG_NUM) match the header. --- .../src/backend/llvm/dyn-handle-tags.test.ts | 39 +++++ packages/compiler/src/backend/llvm/dyn.ts | 4 +- packages/runtime/src/scr_dyn.h | 135 +++++++++--------- packages/runtime/src/scr_message.test.c | 2 +- 4 files changed, 112 insertions(+), 68 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts diff --git a/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts new file mode 100644 index 000000000..8aa69b386 --- /dev/null +++ b/packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "node:fs"; +import { expect, test } from "vitest"; +import { DYN_HANDLE_KINDS } from "../../ir/ir.js"; +import { DYN_HANDLE_TAG_NUM } from "./dyn.js"; + +// ScrDynHandleTag in scr_dyn.h lists its tags by name, each with an explicit +// value, so changes that add tags edit different lines. These checks stand +// in for what the enum's order used to guarantee. +const header = readFileSync(new URL("../../../../runtime/src/scr_dyn.h", import.meta.url), "utf8"); +const body = /typedef enum \{\n([\s\S]*?)\} ScrDynHandleTag;/.exec(header)![1]!; +const tags = [...body.matchAll(/^\s*(SCR_DYNH_[A-Z0-9_]+) = (\d+),/gm)].map(([, name, value]) => ({ + name: name!, + value: Number(value), +})); +const capacity = Number(/#define SCR_DYNH_CAPACITY (\d+)/.exec(header)![1]); + +test("every ScrDynHandleTag has an explicit value, in name order", () => { + const lines = body.split("\n").filter((line) => line.trim().startsWith("SCR_DYNH_")); + expect(tags.length).toBe(lines.length); + expect(tags.map((tag) => tag.name)).toEqual(tags.map((tag) => tag.name).toSorted()); +}); + +test("ScrDynHandleTag values are unique and below SCR_DYNH_CAPACITY", () => { + const owners = new Map(); + for (const { name, value } of tags) { + expect(owners.get(value), `${name} = ${value}`).toBeUndefined(); + owners.set(value, name); + expect(value, name).toBeLessThan(capacity); + } +}); + +test("the backend's handle tag numbers match scr_dyn.h", () => { + const values = new Map(tags.map((tag) => [tag.name, tag.value])); + for (const [kind, num] of Object.entries(DYN_HANDLE_TAG_NUM)) { + const tag = DYN_HANDLE_KINDS.get(kind)?.tag; + expect(tag, kind).toBeDefined(); + expect(values.get(tag!), `${kind} (${tag})`).toBe(num); + } +}); diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 9b11d6240..154b80c02 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -72,8 +72,8 @@ export function recordDynKeepsIdentity(shape: IrRecordShape): boolean { } /** ScrDynKind values (scr_runtime.h). */ -/** ScrDynHandleTag numeric values (scr_runtime.h's enum order). */ -const DYN_HANDLE_TAG_NUM: Record = { +/** ScrDynHandleTag values (scr_dyn.h's explicit values; dyn-handle-tags.test.ts checks them). */ +export const DYN_HANDLE_TAG_NUM: Record = { httpReq: 0, httpRes: 1, netSocket: 2, diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bccd8d55c..b28dfb906 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -117,76 +117,81 @@ typedef enum { /* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the * HANDLE kinds whose member surfaces already have complete static * lowerings (the http/net receiver surface); new kinds join by adding a - * tag + an ops registration in their owning unit. */ + * tag + an ops registration in their owning unit. + * + * Tags are sorted by name and every value is explicit and permanent: the + * LLVM backend hardcodes some of them (DYN_HANDLE_TAG_NUM in + * packages/compiler/src/backend/llvm/dyn.ts). A new tag goes in its sorted + * place with the next value after the largest one in use, below + * SCR_DYNH_CAPACITY. packages/compiler/src/backend/llvm/dyn-handle-tags.test.ts + * checks that values are unique and in range and that the backend's + * numbers match. */ typedef enum { - SCR_DYNH_HTTP_REQ, /* ScrHttpReq — IncomingMessage (server req & client res) */ - SCR_DYNH_HTTP_RES, /* ScrHttpRes — ServerResponse */ - SCR_DYNH_NET_SOCKET, /* ScrNetSocket — net.Socket */ - SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */ - SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */ - SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */ - SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */ - SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */ - SCR_DYNH_ABORT_SIGNAL, /* native static-fetch AbortSignal */ - SCR_DYNH_WEB_STREAM, /* native WHATWG ReadableStream */ - SCR_DYNH_WEB_READER, /* native ReadableStreamDefaultReader */ - SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */ - SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */ - SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */ - SCR_DYNH_EVENT, /* ScrEvent — new Event() and AbortSignal's abort events */ - SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */ - SCR_DYNH_CHILD, /* child_process.ChildProcess */ - SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */ - SCR_DYNH_ARRAY_BUFFER, /* fixed-length ArrayBuffer storage */ - SCR_DYNH_SEGMENTER, /* default Unicode grapheme segmentation */ - SCR_DYNH_SEGMENTS, /* iterable immutable segmentation result */ - SCR_DYNH_BUILTIN_MODULE, /* native builtin export object */ - SCR_DYNH_GLOBAL, /* native global object identity */ - SCR_DYNH_WEAK_MAP, /* native weak-key metadata */ - SCR_DYNH_WEAK_SET, /* native weak membership */ - SCR_DYNH_SET, /* native Set, shared backing map */ - SCR_DYNH_REGEXP, /* native RegExp, shared compiled pattern */ - SCR_DYNH_STDIO, /* stable process stdin/stdout/stderr values */ - SCR_DYNH_URL, /* native WHATWG URL */ - SCR_DYNH_MAP, /* native Map, shared backing */ - SCR_DYNH_DATE, /* JavaScript Date with stable native identity */ - SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */ - SCR_DYNH_CONSOLE, /* stored global console value */ - SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */ - SCR_DYNH_FETCH_REQUEST, - SCR_DYNH_WORKER, - SCR_DYNH_MESSAGE_PORT, - SCR_DYNH_SHARED_ARRAY_BUFFER, - SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */ - SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */ - SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */ - SCR_DYNH_BLOB, /* ScrBlob — new Blob() and Blob#slice() */ - SCR_DYNH_FILE, /* ScrBlob with a name — new File() */ - SCR_DYNH_KEY_OBJECT, /* node:crypto secret KeyObject (scr_lib.c) */ - SCR_DYNH_DIR, /* fs.Dir (scr_lib.c) */ - SCR_DYNH_CIPHER, /* node:crypto Cipheriv (scr_cipher.c) */ - SCR_DYNH_DECIPHER, /* node:crypto Decipheriv (scr_cipher.c) */ - SCR_DYNH_NET_SOCKET_ADDRESS, /* net.SocketAddress (scr_net.c) */ - SCR_DYNH_NET_BLOCK_LIST, /* net.BlockList (scr_net.c) */ - SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */ - SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */ - SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */ - SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ - SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ - SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */ - SCR_DYNH_COUNT, + SCR_DYNH_ABORT_CONTROLLER = 15, /* native static-fetch AbortController */ + SCR_DYNH_ABORT_SIGNAL = 8, /* native static-fetch AbortSignal */ + SCR_DYNH_ARRAY_BUFFER = 18, /* fixed-length ArrayBuffer storage */ + SCR_DYNH_BLOB = 41, /* ScrBlob — new Blob() and Blob#slice() */ + SCR_DYNH_BROADCAST_CHANNEL = 51, /* worker_threads BroadcastChannel (scr_worker_events.c) */ + SCR_DYNH_BUILTIN_MODULE = 21, /* native builtin export object */ + SCR_DYNH_CHILD = 16, /* child_process.ChildProcess */ + SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */ + SCR_DYNH_CONSOLE = 32, /* stored global console value */ + SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */ + SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */ + SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */ + SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */ + SCR_DYNH_DIR = 44, /* fs.Dir (scr_lib.c) */ + SCR_DYNH_DISPOSABLE_STACK = 49, /* DisposableStack (scr_symbol.c) */ + SCR_DYNH_EVENT = 14, /* ScrEvent — new Event() and AbortSignal's abort events */ + SCR_DYNH_EVENT_TARGET = 39, /* ScrEventTarget — new EventTarget() */ + SCR_DYNH_FETCH_HEADERS = 13, /* native static-fetch response Headers */ + SCR_DYNH_FETCH_REQUEST = 34, + SCR_DYNH_FETCH_RESPONSE = 12, /* native static-fetch Response */ + SCR_DYNH_FILE = 42, /* ScrBlob with a name — new File() */ + SCR_DYNH_FILE_HANDLE = 17, /* fs/promises.FileHandle */ + SCR_DYNH_GLOBAL = 22, /* native global object identity */ + SCR_DYNH_H2_SESSION = 4, /* ScrH2Session — Http2Session (client & server) */ + SCR_DYNH_H2_STREAM = 5, /* ScrH2Stream — Http2Stream (client & server) */ + SCR_DYNH_HASH = 52, /* node:crypto Hash (scr_lib.c) */ + SCR_DYNH_HMAC = 53, /* node:crypto Hmac (scr_lib.c) */ + SCR_DYNH_HTTP_AGENT = 7, /* ScrHttpAgent — http.Agent / https.Agent */ + SCR_DYNH_HTTP_CLIENT = 6, /* ScrHttpClientReq — http.ClientRequest */ + SCR_DYNH_HTTP_REQ = 0, /* ScrHttpReq — IncomingMessage (server req & client res) */ + SCR_DYNH_HTTP_RES = 1, /* ScrHttpRes — ServerResponse */ + SCR_DYNH_ITERATOR = 31, /* live native array/string/byte iterator */ + SCR_DYNH_ITERATOR_HELPER = 54, /* an iterator helper object over a checked iterator (scr_json.c) */ + SCR_DYNH_KEY_OBJECT = 43, /* node:crypto secret KeyObject (scr_lib.c) */ + SCR_DYNH_MAP = 29, /* native Map, shared backing */ + SCR_DYNH_MESSAGE_EVENT = 50, /* ScrEvent with data — BroadcastChannel's 'message' */ + SCR_DYNH_MESSAGE_PORT = 36, + SCR_DYNH_NET_BLOCK_LIST = 48, /* net.BlockList (scr_net.c) */ + SCR_DYNH_NET_SERVER = 3, /* ScrNetServer — net.Server (http.Server rides the same handle) */ + SCR_DYNH_NET_SOCKET = 2, /* ScrNetSocket — net.Socket */ + SCR_DYNH_NET_SOCKET_ADDRESS = 47, /* net.SocketAddress (scr_net.c) */ + SCR_DYNH_REGEXP = 26, /* native RegExp, shared compiled pattern */ + SCR_DYNH_SEARCH_PARAMS = 33, /* live WHATWG URLSearchParams */ + SCR_DYNH_SEGMENTER = 19, /* default Unicode grapheme segmentation */ + SCR_DYNH_SEGMENTS = 20, /* iterable immutable segmentation result */ + SCR_DYNH_SET = 25, /* native Set, shared backing map */ + SCR_DYNH_SHARED_ARRAY_BUFFER = 37, + SCR_DYNH_STDIO = 27, /* stable process stdin/stdout/stderr values */ + SCR_DYNH_URL = 28, /* native WHATWG URL */ + SCR_DYNH_WEAK_MAP = 23, /* native weak-key metadata */ + SCR_DYNH_WEAK_SET = 24, /* native weak membership */ + SCR_DYNH_WEB_CONTROLLER = 11, /* native ReadableStreamDefaultController */ + SCR_DYNH_WEB_READER = 10, /* native ReadableStreamDefaultReader */ + SCR_DYNH_WEB_STREAM = 9, /* native WHATWG ReadableStream */ + SCR_DYNH_WORKER = 35, } ScrDynHandleTag; /* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks - * in scr_message.c) is sized by this fixed capacity, never by - * SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes - * no always-linked storage or code: a program that never boxes the new - * handle links byte-identical objects. Raising the capacity is a - * program-wide layout change; do it in a change of its own, measured as - * neutral, and only when the enum outgrows it. */ + * in scr_message.c) is sized by this fixed capacity, never by the number of + * tags. Adding a tag for a link-gated unit therefore changes no + * always-linked storage or code: a program that never boxes the new handle + * links byte-identical objects. Raising the capacity is a program-wide + * layout change; do it in a change of its own, measured as neutral, and + * only when the tags outgrow it. */ #define SCR_DYNH_CAPACITY 128 -_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY, - "ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change"); typedef struct ScrDyn ScrDyn; typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */ diff --git a/packages/runtime/src/scr_message.test.c b/packages/runtime/src/scr_message.test.c index 0c6bd7ae3..a6c940783 100644 --- a/packages/runtime/src/scr_message.test.c +++ b/packages/runtime/src/scr_message.test.c @@ -175,7 +175,7 @@ static void check_documents(void) { /* A link-gated unit's handle crosses threads through the transfer hooks in * its ops: share on the sender, adopt on the receiver, drop with the * message. The test borrows the last capacity slot, which no unit uses, so - * it also proves that slots past SCR_DYNH_COUNT work. */ + * it also proves that slots past the tags in use work. */ #define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1)) typedef struct { _Atomic size_t rc; From dc6a638e63cf1663776e9d72e679d0965fa25015 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:10:17 -0500 Subject: [PATCH 48/67] Lower string and typed array iterators, Iterator.from and helper return() String iterators (s[Symbol.iterator]()) and typed array values(), keys() and entries() compile as iterator objects: generators over the code points and over the indices. The iterator helpers apply to them, and for...of, spreads and Array.from consume them. Iterator.from over an array, string, typed array, Set, Map or iterator object answers the value's own iterator, as Node does for iterators that already inherit from Iterator.prototype. Objects with their own next method keep the Iterator fence. As the root of a helper chain, Iterator.from(x) walks x itself: an array by index, a Set or Map live, a generator directly. h.return!() and h.return?.() compile when h is a helper call or a const initialized with one: helper objects always have return(), so both spellings are the call. Other iterator objects keep the fence, since the array, string, Map and Set iterators have no return(). flatMap callbacks may return Sets (their values) and Maps (their entries), walked live; a string result throws Node's TypeError and closes the source. --- .../builtin-apis/iterator-helpers.mdx | 2 +- .../src/frontend/lowering/containers/bytes.ts | 16 ++ .../lowering/containers/iterator-helpers.ts | 222 +++++++++++++++++- .../frontend/lowering/iterator-adapters.ts | 216 +++++++++++++++++ .../src/frontend/lowering/lower-calls.ts | 81 +++++-- .../src/frontend/lowering/lower-exprs.ts | 5 + packages/compiler/src/frontend/type-mapper.ts | 5 +- .../corpus/6071-iterator-sources.ts.json | 6 + .../corpus/6072-iterator-sources-js.js.json | 6 + .../string-iterator-helpers.ts.json | 6 - tests/corpus/6071-iterator-sources.ts | 70 ++++++ tests/corpus/6072-iterator-sources-js.js | 70 ++++++ .../collection-iterator-helpers.ts | 18 +- tests/diagnostics/es2025-fences.ts | 6 +- tests/diagnostics/string-iterator-helpers.ts | 3 - .../collection-iterator-helpers.ts.txt | 22 +- .../__snapshots__/es2025-fences.ts.txt | 8 +- .../string-iterator-helpers.ts.txt | 8 - tests/harness/corpus-costs.json | 4 + 19 files changed, 700 insertions(+), 74 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json delete mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json create mode 100644 tests/corpus/6071-iterator-sources.ts create mode 100644 tests/corpus/6072-iterator-sources-js.js delete mode 100644 tests/diagnostics/string-iterator-helpers.ts delete mode 100644 tests/harness/__snapshots__/string-iterator-helpers.ts.txt diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index 01f779cb6..cb7cab1f4 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -1 +1 @@ -- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported. +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported. diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index c41bf5163..91cf0b263 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -35,6 +35,7 @@ import { } from "../optional-arguments.js"; import { lowerCheckedArrayFrom } from "./array-construction.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; +import { lowerBytesIteratorValue } from "../iterator-adapters.js"; /** TypedArray.prototype.sort and toSorted. The receiver and comparator * expressions are evaluated before entering the helper (buildBytesSortFn). @@ -462,6 +463,21 @@ export function lowerBytesMethodCall( if (name === "sort" || name === "toSorted") { return lowerBytesSortCall(lowerer, call, access, receiverIr); } + // values()/keys()/entries() as iterator objects (for...of heads and + // stored numeric iterators lower before this). + if ((name === "values" || name === "keys" || name === "entries") && nArgs === 0) { + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (type?.kind === "generator") { + const value = lowerBytesIteratorValue( + lowerer, + lowerer.lowerExpr(access.expression), + name, + type, + loc, + ); + if (value) return value; + } + } if (name === "toReversed") { if (nArgs !== 0) { lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 7cb3753ac..70e0d85e0 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -5,6 +5,7 @@ import type { Lowerer } from "../lowerer.js"; import { BOOL, CAUGHT, + DYN, F64, type IrMapIntrinsicMethod, type IrSetIntrinsicMethod, @@ -28,12 +29,18 @@ import { concatStrings, throwTypeError } from "./dynamic-receivers.js"; import { type GenType, generatorLoopBinding, + resultRecordOf, genStep, genValue, stepDrivable, } from "../lower-generators.js"; import { exitBeforeReturns } from "./for-of.js"; -import { iteratorSlotAdapter } from "../iterator-adapters.js"; +import { + iteratorSlotAdapter, + lowerArrayIteratorValue, + lowerBytesIteratorValue, + lowerStringIteratorValue, +} from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; /* ── iterator helpers (ES2025) ─────────────────────────────────────────── */ @@ -189,6 +196,24 @@ export function lowerGeneratorHelperCall( collectionSeed(lowerer, source), ); } + // Iterator.from(arr) walks the array by index. + const arrayRoot = arrayIteratorCall(lowerer, cur); + if (arrayRoot !== null) { + const items = lowerer.lowerExpr(arrayRoot.receiver); + if (items.type.kind !== "array") + lowerer.badType(arrayRoot.receiver, lowerer.typeOf(arrayRoot.receiver)); + const elem = + arrayRoot.proj === "entries" ? entryPairType(lowerer, items.type.elem) : items.type.elem; + return lowerFusedChain( + lowerer, + call, + name, + stages, + { kind: "array", type: items.type, proj: arrayRoot.proj }, + items, + elem, + ); + } const src = lowerHelperSource(lowerer, cur); return lowerFusedChain( lowerer, @@ -201,6 +226,128 @@ export function lowerGeneratorHelperCall( ); } +/** `h.return!()` or `h.return?.()` on a helper object. The lib declares + * return() as optional on iterator objects, and a helper object always + * has one, so both spellings are the call: the helper completes and closes + * its source, and the result is `{ value: undefined, done: true }`. The + * receiver must be provably a helper object — a helper stage call, or a + * const binding initialized with one — since other iterator objects (the + * array, string, Map and Set iterators) have no return(). Null otherwise, + * and for the plain `h.return()` spelling (lowerGenMethodCall's). */ +export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + let callee: ts.Expression = call.expression; + const nonNull = ts.isNonNullExpression(callee); + if (nonNull) callee = (callee as ts.NonNullExpression).expression; + if (!nonNull && call.questionDotToken === undefined) return null; + if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; + if (callee.name.text !== "return" || call.arguments.length !== 0) return null; + if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null; + const callT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const gen = lowerer.lowerExpr(callee.expression); + if (gen.type.kind !== "generator" || gen.type.async) return null; + const loc = locOf(call); + const recT = resultRecordOf(lowerer, gen.type); + const resume: IrExpr = { kind: "genResume", mode: "return", gen, arg: null, type: recT, loc }; + // `?.` types the call `IteratorResult | undefined`, which has no mapping + // as a whole; the result is never undefined, so it is the record itself. + if (callT === null || typeEquals(callT, recT)) return resume; + const result = lowerer.coerceToExpected(resume, callT); + if (!typeEquals(result.type, callT)) lowerer.badType(call, lowerer.typeOf(call)); + return result; +} + +/** Whether `expr` is a helper stage call, or a const binding initialized + * with one. */ +function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (isStageCall(lowerer, expr)) return true; + if (!ts.isIdentifier(expr)) return false; + const symbol = lowerer.resolveValueSymbol(expr); + const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + if ( + !decl || + !ts.isVariableDeclaration(decl) || + decl.initializer === undefined || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & ts.NodeFlags.Const) === 0 + ) + return false; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + return isStageCall(lowerer, init); +} + +/** `Iterator.from(value)` over a typed built-in iterable: the value's own + * iterator, which already inherits from Iterator.prototype, so Node returns + * it rather than a wrapper. A generator (including a helper object or an + * array iterator) is itself; an array, string or typed array gives its + * values iterator; a Set its values and a Map its entries, adapted into a + * generator over the live collection. The result converts into the call's + * IteratorObject type. Null for other arguments (objects with their own + * next method, untyped values), which keep the Iterator fence. */ +export function lowerIteratorFromCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "from" || call.arguments.length !== 1) return null; + const argNode = call.arguments[0]!; + if (ts.isSpreadElement(argNode)) return null; + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); + if (resultT?.kind !== "generator" || resultT.async || argT === null) return null; + const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"]; + if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null; + const loc = locOf(call); + const value = lowerer.lowerExpr(argNode); + const own = (yieldT: IrType): GenType => ({ + kind: "generator", + yieldT, + retT: VOID, + nextT: UNDEFINED_T, + }); + let iterator: IrExpr | null = null; + const t = value.type; + if (t.kind === "generator") iterator = value; + else if (t.kind === "array") + iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc); + else if (t.kind === "string") + iterator = lowerStringIteratorValue(lowerer, value, own(STRING), loc); + else if (t.kind === "bytes") + iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc); + else if (t.kind === "set" || t.kind === "map") { + const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value); + const native: IrExpr = { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(value, DYN), + method: t.kind === "set" ? "values" : "entries", + calleeName: t.kind === "set" ? "set.values" : "map.entries", + args: [], + type: DYN, + loc, + }; + iterator = iteratorSlotAdapter(lowerer, native, own(elem)); + } + if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode)); + const result = lowerer.coerceToExpected(iterator, resultT); + if (!typeEquals(result.type, resultT)) lowerer.badType(call, lowerer.typeOf(call)); + return result; +} + +/** The interned [K, V] pair a Map's entries yield. */ +function entryPairTypeOf(lowerer: Lowerer, keyT: IrType, valueT: IrType): IrType { + return { + kind: "record", + shapeId: lowerer.shapes.intern( + [ + { name: "0", type: keyT }, + { name: "1", type: valueT }, + ], + true, + ), + }; +} + const ITER_STAGES = new Set(["map", "filter", "take", "drop", "flatMap"]); const ITER_TERMINALS = new Set(["toArray", "forEach", "reduce", "some", "every", "find"]); /** Native iterators that ride checked runtime values and adapt into @@ -353,12 +500,30 @@ export function lowerForOfHelperChain( } } -/** `arr.values()` or `arr.entries()` on an array value, as the root of a - * helper chain. */ +/** The argument of an `Iterator.from(x)` call, or null. As a chain's root, + * the call is the argument's own iterator, so the chain can walk the + * argument directly. */ +function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null { + if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; + if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1) + return null; + const arg = expr.arguments[0]!; + if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null; + if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null; + return lowerer.isStdlibMember(expr.expression) ? arg : null; +} + +/** `arr.values()`, `arr.entries()` or `Iterator.from(arr)` on an array value, + * as the root of a helper chain. */ function arrayIteratorCall( lowerer: Lowerer, expr: ts.Expression, ): { receiver: ts.Expression; proj: "values" | "entries" } | null { + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null) + return lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "array" + ? { receiver: fromArg, proj: "values" } + : null; if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) return null; @@ -401,6 +566,17 @@ function collectionIteratorCall( lowerer: Lowerer, expr: ts.Expression, ): { receiver: ts.Expression; proj: "keys" | "values" | "entries" } | null { + // Iterator.from(set) is the Set's values iterator, Iterator.from(map) the + // Map's entries iterator. + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null) { + const kind = lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind; + return kind === "set" + ? { receiver: fromArg, proj: "values" } + : kind === "map" + ? { receiver: fromArg, proj: "entries" } + : null; + } if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) return null; @@ -480,6 +656,11 @@ function lowerHelperSource( lowerer: Lowerer, expr: ts.Expression, ): { gen: IrExpr & { type: GenType }; elem: IrType } { + // Iterator.from(gen) is the generator itself; the call's IteratorObject + // type would add an adapter generator over it. + const fromArg = iteratorFromArgument(lowerer, expr); + if (fromArg !== null && lowerer.mapTypeOf(lowerer.typeOf(fromArg))?.kind === "generator") + return lowerHelperSource(lowerer, fromArg); const tsType = lowerer.typeOf(expr); const mapped = lowerer.mapTypeOf(tsType); const arg = lowerer.checker.getTypeArguments(tsType as ts.TypeReference)[0]; @@ -577,10 +758,17 @@ function lowerStage( if (ret.kind === "array") return { stage: { kind: "flatMap", fn }, out: ret.elem }; if (ret.kind === "generator" && ret.async !== true && stepDrivable(ret)) return { stage: { kind: "flatMap", fn }, out: ret.yieldT }; + // A Set flattens its values and a Map its entries, walked live. A string + // is rejected at run time with Node's TypeError (strings are not + // iterator objects). + if (ret.kind === "set") return { stage: { kind: "flatMap", fn }, out: ret.elem }; + if (ret.kind === "map") + return { stage: { kind: "flatMap", fn }, out: entryPairTypeOf(lowerer, ret.key, ret.value) }; + if (ret.kind === "string") return { stage: { kind: "flatMap", fn }, out: STRING }; lowerer.noLowering( `.flatMap over a callback returning '${lowerer.fmt(ret)}'`, s.argNode, - "callbacks returning an array or a generator are the supported forms", + "callbacks returning an array, a generator, a Set or a Map are the supported forms", ); } @@ -1245,8 +1433,9 @@ function walkCollection( } /** A flatMap stage's inner walk over the callback's result in local - * `innerId`: an array by index, or a generator driven (and closed on - * early exit) like the source. `stop` ends the walk early. */ + * `innerId`: an array by index, a Set's values or a Map's entries walked + * live, or a generator driven (and closed on early exit) like the source. + * A string result throws Node's TypeError. `stop` ends the walk early. */ function walkInner( lowerer: Lowerer, frame: ChainFrame, @@ -1257,6 +1446,27 @@ function walkInner( loc: SrcLoc, ): IrStmt[] { if (innerT.kind === "array") return [walkArray(frame, innerId, innerT, stop, body, loc)]; + if (innerT.kind === "set" || innerT.kind === "map") { + const source: ChainSource = { + kind: "collection", + type: innerT, + proj: innerT.kind === "set" ? "values" : "entries", + }; + return walkCollection(lowerer, frame, innerId, source, stop ?? boolLit(false, loc), body, loc); + } + if (innerT.kind === "string") { + return [ + throwTypeError( + { + kind: "strLit", + value: "Iterator.prototype.flatMap called on non-object", + type: STRING, + loc, + }, + loc, + ), + ]; + } if (innerT.kind !== "generator") throw new InternalCompilerError("lowerer bug: flatMap over a non-iterable result"); const pre: IrStmt[] = diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index fb59c4cee..812f29242 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -265,6 +265,222 @@ function dynIteratorAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a + * value: an interned generator over the indices, reading the length and + * the element at each step, as the typed array iterator does. Null when + * `type` is not the matching number (or [number, number]) generator. */ +export function lowerBytesIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + method: "values" | "keys" | "entries", + type: IrType, + loc: SrcLoc, +): IrExpr | null { + const bytesT = receiver.type; + if (bytesT.kind !== "bytes" || type.kind !== "generator") return null; + if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null; + const pair = type.yieldT.kind === "record" ? lowerer.shapes.get(type.yieldT.shapeId) : undefined; + const pairOk = + pair?.tuple === true && + pair.fields.length === 2 && + pair.fields.every((f) => (f.name === "0" || f.name === "1") && f.type.kind === "f64"); + if (method === "entries" ? !pairOk : type.yieldT.kind !== "f64") return null; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = `%bytes.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + const list = varRef("list.0", bytesT, loc); + const index = varRef("index.0", F64, loc); + const element: IrExpr = { + kind: "bytesIntrinsic", + method: "get", + receiver: list, + args: [index], + type: F64, + loc, + }; + const yielded: IrExpr = + method === "keys" + ? index + : method === "values" + ? element + : { + kind: "recordLit", + fields: [ + { name: "0", value: index }, + { name: "1", value: element }, + ], + type: type.yieldT, + loc, + }; + lowerer.liftedFns.push({ + name, + params: [{ localId: "list.0", name: "list", type: bytesT }], + returnType: VOID, + locals: [ + { id: "list.0", name: "list", type: bytesT, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { kind: "numLit", value: 0, type: F64, loc }, + loc, + }, + { + kind: "while", + cond: { + kind: "bin", + op: "<", + left: index, + right: { + kind: "bytesIntrinsic", + method: "length", + receiver: list, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + body: [ + { kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc }, + { + kind: "assign", + localId: "index.0", + value: { + kind: "bin", + op: "+", + left: index, + right: { kind: "numLit", value: 1, type: F64, loc }, + type: F64, + loc, + }, + loc, + }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + +/** `s[Symbol.iterator]()` on a string value: an interned generator over + * its code points, the string iterator's steps (a lone surrogate is one + * step). Null when `type` is not the plain string-yielding generator. */ +export function lowerStringIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + type: IrType, + loc: SrcLoc, +): IrExpr | null { + if (receiver.type.kind !== "string" || type.kind !== "generator") return null; + if ( + type.yieldT.kind !== "string" || + type.retT.kind !== "void" || + type.nextT.kind !== "undefinedT" + ) + return null; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `strIter:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + name = `%str.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + const text = varRef("text.0", STRING, loc); + const index = varRef("index.0", F64, loc); + const point = varRef("point.0", STRING, loc); + lowerer.liftedFns.push({ + name, + params: [{ localId: "text.0", name: "text", type: STRING }], + returnType: VOID, + locals: [ + { id: "text.0", name: "text", type: STRING, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + { id: "point.0", name: "point", type: STRING, mutable: false }, + ], + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { kind: "numLit", value: 0, type: F64, loc }, + loc, + }, + { + kind: "while", + cond: { + kind: "bin", + op: "<", + left: index, + right: { + kind: "strIntrinsic", + method: "length", + receiver: text, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + body: [ + { + kind: "varDecl", + localId: "point.0", + init: { + kind: "strIntrinsic", + method: "cpAt", + receiver: text, + args: [index], + type: STRING, + loc, + }, + loc, + }, + { + kind: "assign", + localId: "index.0", + value: { + kind: "bin", + op: "+", + left: index, + right: { + kind: "strIntrinsic", + method: "length", + receiver: point, + args: [], + type: F64, + loc, + }, + type: F64, + loc, + }, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value: point, type: VOID, loc }, loc }, + ], + loc, + }, + { kind: "return", value: null, loc }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + /** A generator flowing into a slot with other channel types (a * `Generator` returned through an * `IterableIterator` annotation). The interned adapter generator diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 23b8b69d2..58cbb9543 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -94,6 +94,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/ import { lowerGroupByStaticCall } from "./containers/collection-grouping.js"; import { lowerGeneratorHelperCall, + lowerHelperReturnCall, + lowerIteratorFromCall, lowerIteratorHelperCall, } from "./containers/iterator-helpers.js"; import { @@ -223,6 +225,7 @@ import { completeArgs, } from "./call-arguments.js"; import { isThisParameter, type ParamShape } from "./call-signatures.js"; +import { lowerStringIteratorValue } from "./iterator-adapters.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js"; import { @@ -365,6 +368,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // machinery (the member always exists on a POSIX target, so the // optional call IS the call; `process.getuid` itself has no value // lowering for the chain to guard). + // `h.return!()` / `h.return?.()` on a helper object: the method exists. + const helperReturn = lowerHelperReturnCall(lowerer, expr); + if (helperReturn) return helperReturn; const processOptional = lowerer.lowerProcessOptionalMethodCall(expr); if (processOptional) return processOptional; const reqStreamOptional = lowerCompatReqStreamOptionalCall(lowerer, expr); @@ -1616,6 +1622,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`); } + // `s[Symbol.iterator]()` on a string: the string iterator as a value. + if ( + ts.isElementAccessExpression(expr.expression) && + !expr.expression.questionDotToken && + !expr.questionDotToken && + expr.arguments.length === 0 + ) { + const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression); + if (iterator) return iterator; + } + // The element spelling of an unsupported native Web method must fence // before lowering the callee into a checked-dynamic keyed read. if (ts.isElementAccessExpression(expr.expression)) { @@ -3157,10 +3174,32 @@ function lowerOptionalStringNumber( return { kind: "call", callee: helper, args: [arg], type: F64, loc }; } -/** Iterator types with checked runtime receivers: Map/Set iterators' - * helper calls lower in lowerGeneratorHelperCall; the string iterator's - * have no lowering. */ -const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]); +/** `s[Symbol.iterator]()` on a string-typed receiver: the generator over + * its code points. Null for other element calls. */ +function lowerStringIteratorCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.ElementAccessExpression, +): IrExpr | null { + let key = access.argumentExpression; + while (ts.isParenthesizedExpression(key)) key = key.expression; + if ( + !ts.isPropertyAccessExpression(key) || + lowerer.stdlibGlobalMember(key, "Symbol") !== "iterator" + ) + return null; + if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null; + const type = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (type?.kind !== "generator") return null; + const receiver = lowerer.lowerExpr(access.expression); + const value = lowerStringIteratorValue(lowerer, receiver, type, locOf(call)); + if (!value) lowerer.unsupported("SC1090", call, "this string iterator type"); + return value; +} + +/** Iterator types with checked runtime receivers, whose helper calls lower + * in lowerGeneratorHelperCall. */ +const CHECKED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator"]); const ITERATOR_HELPER_METHODS = new Set([ "map", "filter", @@ -3220,25 +3259,17 @@ function lowerDynReceiverMethodCall( const recvSymbol = recvTs.getSymbol(); if ( recvSymbol && - UNLOWERED_ITERATOR_TYPES.has(recvSymbol.name) && + CHECKED_ITERATOR_TYPES.has(recvSymbol.name) && lowerer.isStdlibSymbol(recvSymbol) && ITERATOR_HELPER_METHODS.has(access.name.text) && lowerer.isStdlibMember(access) ) { - if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") { - const helper = lowerGeneratorHelperCall(lowerer, call, access); - if (helper) return helper; - lowerer.noLowering( - `collection iterator .${access.name.text}()`, - call, - "stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering", - ); - } - // The checked runtime's string iterator has no helper methods. + const helper = lowerGeneratorHelperCall(lowerer, call, access); + if (helper) return helper; lowerer.noLowering( - `${recvSymbol.name}.${access.name.text}`, + `collection iterator .${access.name.text}()`, call, - "iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()", + "iterator helpers on Map/Set iterators are called directly on the iterator, without optional chaining", ); } const arrayReceiver = lowerer.checker.isArrayType(recvTs); @@ -6644,10 +6675,10 @@ function recordAssignHelper( return helper; } -/** The `Iterator` global's statics (ES2025 — Iterator.from, and the - * abstract constructor as a value) have no lowering: every member fences - * with the working spelling named instead of the generic-method fence's - * monomorphization wording. */ +/** The `Iterator` global's statics (ES2025): `Iterator.from` over a typed + * built-in iterable lowers (lowerIteratorFromCall); every other member, + * and other arguments, fence with the working spelling named instead of + * the generic-method fence's monomorphization wording. */ function lowerIteratorStaticFence( lowerer: Lowerer, call: ts.CallExpression, @@ -6655,11 +6686,15 @@ function lowerIteratorStaticFence( ): IrExpr | null { if (!lowerer.isStdlibGlobal(access.expression, "Iterator")) return null; if (!lowerer.isStdlibMember(access)) return null; + const from = lowerIteratorFromCall(lowerer, call, access); + if (from) return from; lowerer.noLowering( `Iterator.${access.name.text}`, call, - "iterator helpers apply directly to generators and to array, Map and Set iterators: " + - "arr.values().map(f).take(n).toArray()", + access.name.text === "from" + ? "Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators" + : "iterator helpers apply directly to generators and to array, Map and Set iterators: " + + "arr.values().map(f).take(n).toArray()", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index a613a2af0..32f80ef5b 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -147,6 +147,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; +import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { arrayNumberRead, arrayValueRead, @@ -1904,6 +1905,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false); if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr); if (ts.isCallExpression(expr)) { + // `h.return?.()` on a helper object: the method exists, so the + // optional call is the call. + const helperReturn = lowerHelperReturnCall(lowerer, expr); + if (helperReturn) return helperReturn; // The tail of an optional chain whose `?.` sits deeper // (`x?.trim().toLowerCase()`): the whole tail short-circuits together. if (isOptionalChainTail(lowerer, expr)) return lowerer.lowerOptionalChain(expr); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index d61464b07..41d18a6a2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -3267,8 +3267,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Native collection iterators use checked runtime iterator objects. if (isStdlibInterface("MapIterator") || isStdlibInterface("SetIterator")) return DYN; // Array iterators (`list.values()` and friends) are generators over the - // live array: each step reads the current length and element. - if (isStdlibInterface("ArrayIterator")) { + // live array: each step reads the current length and element. String + // iterators (`s[Symbol.iterator]()`) are generators over the code points. + if (isStdlibInterface("ArrayIterator") || isStdlibInterface("StringIterator")) { const arg = checker.getTypeArguments(widened as ts.TypeReference)[0]; const yieldT = arg ? mapType(arg, ctx) : null; if (!yieldT || yieldT.kind === "void" || isUnitType(yieldT)) return null; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json new file mode 100644 index 000000000..2745f7978 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6071-iterator-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6071-iterator-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json new file mode 100644 index 000000000..e89c895e4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6072-iterator-sources-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6072-iterator-sources-js.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json deleted file mode 100644 index 0a716bec5..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/string-iterator-helpers.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/string-iterator-helpers.ts" - ], - "diags": [] -} diff --git a/tests/corpus/6071-iterator-sources.ts b/tests/corpus/6071-iterator-sources.ts new file mode 100644 index 000000000..c63469022 --- /dev/null +++ b/tests/corpus/6071-iterator-sources.ts @@ -0,0 +1,70 @@ +// Iterator objects from strings and typed arrays, Iterator.from over the +// built-in iterables, return() on helper objects, and flatMap over Set, +// Map and string results. +const log: string[] = []; +function* count(label: string, n: number): Generator { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what: unknown): void { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// String iterators step by code point; a lone surrogate is one step. +const text = "a\u{1F600}b\uD800"; +const chars = text[Symbol.iterator](); +console.log(chars.next(), chars.next().value?.length, [...chars].length); +console.log(text[Symbol.iterator]().map((c) => c.codePointAt(0)!.toString(16)).toArray()); +console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join("")); +for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c); +console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h")); + +// Typed array iterators. +const bytes = new Uint8Array([9, 8, 7]); +console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray()); +console.log(bytes.entries().map(([i, v]) => i * v).toArray()); +const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1); +console.log(floats.next(), [...floats]); +for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v); + +// Iterator.from returns each built-in iterable's own iterator. +const list = [1, 2]; +const fromList = Iterator.from(list); +list.push(3); +console.log([...fromList], Iterator.from("a\u{1F600}").toArray()); +console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray()); +console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0)); +const fromGen = Iterator.from(count("a", 3)); +flush([fromGen.next(), fromGen.take(1).toArray()]); +flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray()); + +// return() on helper objects. +const started = count("c", 3); +started.next(); +const doubled = started.map((x) => x * 2); +flush([doubled.return!(), started.next()]); +const fresh = count("d", 3); +const kept = fresh.filter(() => true); +flush([kept.return?.(), fresh.next(), kept.next()]); +const stepping = count("e", 5).drop(1); +flush([stepping.next(), stepping.return!(), stepping.next()]); +flush(count("f", 4).map((x) => x).take(2).return!()); + +// flatMap over Set and Map results, and a string result's TypeError. +console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray()); +console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray()); +const pairs = new Map([["m", 1], ["n", 2]]); +for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v); +try { + count("g", 3).flatMap((x) => `s${x}`).toArray(); +} catch (e) { + log.push(`${(e as Error).name}: ${(e as Error).message}`); +} +flush("string result"); diff --git a/tests/corpus/6072-iterator-sources-js.js b/tests/corpus/6072-iterator-sources-js.js new file mode 100644 index 000000000..84dd00067 --- /dev/null +++ b/tests/corpus/6072-iterator-sources-js.js @@ -0,0 +1,70 @@ +// The JavaScript form: iterator objects from strings and typed arrays, +// Iterator.from over the built-in iterables, return() on helper objects, +// and flatMap over Set, Map and string results. +const log = []; +function* count(label , n ) { + try { + for (let i = 1; i <= n; i++) { + log.push(`${label}${i}`); + yield i; + } + } finally { + log.push(`${label}:closed`); + } +} +function flush(what ) { + console.log(what, "|", log.join(" ")); + log.length = 0; +} + +// String iterators step by code point; a lone surrogate is one step. +const text = "a\u{1F600}b\uD800"; +const chars = text[Symbol.iterator](); +console.log(chars.next(), chars.next().value.length, [...chars].length); +console.log(text[Symbol.iterator]().map((c) => c.length).toArray()); +console.log("naïve"[Symbol.iterator]().filter((c) => c !== "ï").toArray().join("")); +for (const c of "xyz"[Symbol.iterator]().drop(1)) console.log("char", c); +console.log("hello"[Symbol.iterator]().some((c) => c === "l"), "hello"[Symbol.iterator]().find((c) => c > "h")); + +// Typed array iterators. +const bytes = new Uint8Array([9, 8, 7]); +console.log(bytes.values().map((x) => x * 2).toArray(), bytes.keys().filter((i) => i > 0).toArray()); +console.log(bytes.entries().map(([i, v]) => i * v).toArray()); +const floats = new Float64Array([0.5, 1.5, 2.5]).values().map((x) => x + 1); +console.log(floats.next(), [...floats]); +for (const [i, v] of new Int16Array([-1, 2, -3, 4]).entries().filter(([, v]) => v > 0)) console.log(i, v); + +// Iterator.from returns each built-in iterable's own iterator. +const list = [1, 2]; +const fromList = Iterator.from(list); +list.push(3); +console.log([...fromList], Iterator.from("a\u{1F600}").toArray()); +console.log(Iterator.from(new Set(["p", "q"])).toArray(), Iterator.from(new Map([["k", 1]])).toArray()); +console.log(Iterator.from(new Uint8Array([4, 5])).reduce((a, b) => a + b, 0)); +const fromGen = Iterator.from(count("a", 3)); +flush([fromGen.next(), fromGen.take(1).toArray()]); +flush(Iterator.from(count("b", 9).map((x) => x * 3)).take(2).toArray()); + +// return() on helper objects. +const started = count("c", 3); +started.next(); +const doubled = started.map((x) => x * 2); +flush([doubled.return(), started.next()]); +const fresh = count("d", 3); +const kept = fresh.filter(() => true); +flush([kept.return?.(), fresh.next(), kept.next()]); +const stepping = count("e", 5).drop(1); +flush([stepping.next(), stepping.return(), stepping.next()]); +flush(count("f", 4).map((x) => x).take(2).return()); + +// flatMap over Set and Map results, and a string result's TypeError. +console.log([1, 2].values().flatMap((x) => new Set([x, x * 10])).toArray()); +console.log([1, 2].values().flatMap((x) => new Map([[`k${x}`, x]])).toArray()); +const pairs = new Map ([["m", 1], ["n", 2]]); +for (const [k, v] of [0].values().flatMap(() => pairs)) console.log(k, v); +try { + count("g", 3).flatMap((x) => `s${x}`).toArray(); +} catch (e) { + log.push(`${e.name}: ${e.message}`); +} +flush("string result"); diff --git a/tests/diagnostics/collection-iterator-helpers.ts b/tests/diagnostics/collection-iterator-helpers.ts index 127b5c64b..d204e05c3 100644 --- a/tests/diagnostics/collection-iterator-helpers.ts +++ b/tests/diagnostics/collection-iterator-helpers.ts @@ -1,9 +1,13 @@ -// Iterator helpers lower over Map/Set iterators, generators and array -// iterators. These helper forms are refused explicitly. +// Iterator helpers lower over Map/Set iterators, generators and array, +// string and typed array iterators. These helper forms are refused +// explicitly. const set = new Set([1, 2]); -// flatMap callbacks return arrays or generators. -set.values().flatMap((value) => new Set([value, value + 1])); -// return() on a helper object. -const mapped = set.values().map((value) => value * 2); -mapped.return!(); +// return() on an iterator object that is not provably a helper object +// (array, string, Map and Set iterators have no return method). +function close(it: IteratorObject): void { + it.return!(); +} +close(set.values().map((value) => value * 2)); +// throw() on a helper object (helpers have no throw method). +set.values().map((value) => value + 1).throw!(new Error("stop")); // Each helper call must produce a diagnostic. diff --git a/tests/diagnostics/es2025-fences.ts b/tests/diagnostics/es2025-fences.ts index 067da192e..1d141eb84 100644 --- a/tests/diagnostics/es2025-fences.ts +++ b/tests/diagnostics/es2025-fences.ts @@ -1,6 +1,6 @@ // The es2025-lib surface WITHOUT a lowering fences by name: resizable // ArrayBuffers and transfer, SharedArrayBuffer.grow, Atomics.waitAsync, -// first-class iterator objects, and the unlowered forms of the +// Iterator.from over objects with their own next method, and the unlowered forms of the // implemented statics (Promise.try with fewer arguments than parameters, // groupBy's literal-union keys, a set-like non-Set argument). @@ -21,8 +21,8 @@ sab.grow(16); const ia = new Int32Array(4); const waited = Atomics.waitAsync(ia, 0, 0, 1); -// First-class iterator objects. -const it = Iterator.from([1, 2, 3].values()); +// Iterator.from over an object with its own next method. +const it = Iterator.from({ next: (): IteratorResult => ({ value: 1, done: false }) }); // Promise.try with fewer arguments than the callback's parameters. const tried = Promise.try((a: number, b?: number) => a + (b ?? 0), 1); diff --git a/tests/diagnostics/string-iterator-helpers.ts b/tests/diagnostics/string-iterator-helpers.ts deleted file mode 100644 index ed0e2315f..000000000 --- a/tests/diagnostics/string-iterator-helpers.ts +++ /dev/null @@ -1,3 +0,0 @@ -// The string iterator has no iterator-helper lowering; the call is -// refused instead of failing at run time. -const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); diff --git a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt index 87470b621..9c56aa958 100644 --- a/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt +++ b/tests/harness/__snapshots__/collection-iterator-helpers.ts.txt @@ -1,17 +1,17 @@ -collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set'' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:8:3 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet - 4 | // flatMap callbacks return arrays or generators. - 5 | set.values().flatMap((value) => new Set([value, value + 1])); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 6 | // return() on a helper object. + 7 | function close(it: IteratorObject): void { + 8 | it.return!(); + | ^~~~~~~~~ + 9 | } - hint: callbacks returning an array or a generator are the supported forms + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject.return' is part of the standard library types but has no scriptc lowering yet +collection-iterator-helpers.ts:12:1 - error SC2020: 'IteratorObject.throw' is part of the standard library types but has no scriptc lowering yet - 7 | const mapped = set.values().map((value) => value * 2); - 8 | mapped.return!(); - | ^~~~~~~~~~~~~ - 9 | // Each helper call must produce a diagnostic. + 11 | // throw() on a helper object (helpers have no throw method). + 12 | set.values().map((value) => value + 1).throw!(new Error("stop")); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 13 | // Each helper call must produce a diagnostic. 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 diff --git a/tests/harness/__snapshots__/es2025-fences.ts.txt b/tests/harness/__snapshots__/es2025-fences.ts.txt index d7eda27b6..b1ad939fe 100644 --- a/tests/harness/__snapshots__/es2025-fences.ts.txt +++ b/tests/harness/__snapshots__/es2025-fences.ts.txt @@ -45,12 +45,12 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet - 24 | // First-class iterator objects. - 25 | const it = Iterator.from([1, 2, 3].values()); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 24 | // Iterator.from over an object with its own next method. + 25 | const it = Iterator.from({ next: (): IteratorResult => ({ value: 1, done: false }) }); + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | - hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray() + hint: Iterator.from accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt b/tests/harness/__snapshots__/string-iterator-helpers.ts.txt deleted file mode 100644 index ef768934e..000000000 --- a/tests/harness/__snapshots__/string-iterator-helpers.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -string-iterator-helpers.ts:3:15 - error SC2020: 'StringIterator.map' is part of the standard library types but has no scriptc lowering yet - - 2 | // refused instead of failing at run time. - 3 | const upper = "abc"[Symbol.iterator]().map((c) => c.toUpperCase()); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 4 | - - hint: iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values() \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 000efcdfc..0439e2afb 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2234,6 +2234,8 @@ "6053-class-to-primitive.ts": 4385, "6054-js-class-to-primitive.js": 2181, "6070-iterator-helpers-untyped.js": 4050, + "6071-iterator-sources.ts": 5150, + "6072-iterator-sources-js.js": 5350, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5304,6 +5306,8 @@ "6053-class-to-primitive.ts": 36270, "6054-js-class-to-primitive.js": 2958, "6070-iterator-helpers-untyped.js": 6900, + "6071-iterator-sources.ts": 5150, + "6072-iterator-sources-js.js": 5350, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From 15636ecf90565dc78c8a42ce199f5a4c66a0d526 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:18:10 -0500 Subject: [PATCH 49/67] List bench/thread-slots among the zero-cost gate's dynamic workloads The workload's release binary links dynamic-tier symbols through its worker messages: node:worker_threads values (Worker, parentPort, workerData) are dyn objects, and worker.on and postMessage lower to dynamic method calls. ThreadSlots and InternTable link none on their own. intern-table and typed-array-scan-workers are listed for the same reason. --- scripts/zero-cost-gate.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/zero-cost-gate.mjs b/scripts/zero-cost-gate.mjs index f4272ac6a..297e741e5 100644 --- a/scripts/zero-cost-gate.mjs +++ b/scripts/zero-cost-gate.mjs @@ -316,7 +316,9 @@ const definedFunctions = (binary) => * b14377864 (DYNAMIC_TIER_PREFIXES in zero-cost-compare.mjs): JSON.parse * trees, worker messages, regex exec metadata (two-tier §3.3 L5), and a * shared scr_dyn_call/scr_dyn_define_property core not yet traced to its - * typed root. Every other bench workload is fully typed and must link none. + * typed root. bench/thread-slots, added later, links them through worker + * messages: node:worker_threads values are dyn objects. Every other bench + * workload is fully typed and must link none. * Remove a workload here when a leak fix makes its binary clean. */ const DYNAMIC_WORKLOADS = new Set([ "bench/json-records", @@ -326,6 +328,7 @@ const DYNAMIC_WORKLOADS = new Set([ "bench/regex-logs", "bench/atomics-contended", "bench/intern-table", + "bench/thread-slots", "bench/typed-array-scan-workers", "bench/log-summary", "bench/inventory-report", From 1f7f029a05f2d753fffb20a89386cd7645c92c8c Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:26:09 -0500 Subject: [PATCH 50/67] Carry AsyncLocalStorage stores into timer, immediate, tick and microtask callbacks AsyncLocalStorage changes, matching Node 24.22: - setTimeout, setInterval, setImmediate, process.nextTick and queueMicrotask callbacks run with the stores that were active when they were scheduled. Before, getStore() returned undefined in them. - An interval keeps that context on every tick, and a refreshed timer keeps it too. - enterWith inside one of these callbacks lasts until the callback returns. An enterWith in top-level code ends when the main module's body returns, and one inside a callback scheduled with no store active ends when that callback returns, so the store does not reach callbacks that were scheduled without it. Programs that enter stores (store.run, exit, enterWith, or a diagnostics_channel store binding) schedule through new *_als runtime entry points, which wrap a callback scheduled with a store active in a closure that installs the stores around the call. The emitter picks them from the new asyncContext runtime feature; other programs keep the plain entry points, so their scheduling is unchanged. The limitations page notes that completion callbacks of callback-style I/O APIs still run with no store. 5815-als-callback-context compares the stores each kind of callback sees with Node, plain and with SCRIPTC_SAN=1. --- docs/content/docs/limitations.mdx | 2 + .../builtin-apis/asynclocalstorage.mdx | 1 + .../generated/node-v24-backlog.json | 21 ++-- .../generated/node-v24-internal.json | 25 +++-- internal/compatibility/static-support.json | 4 +- packages/compiler/src/backend/llvm/emitter.ts | 8 ++ .../compiler/src/backend/llvm/lib-dispatch.ts | 8 +- .../compiler/src/backend/llvm/lib-process.ts | 20 ++-- packages/compiler/src/ir/runtime-features.ts | 17 +++ .../corpus/5815-als-callback-context.ts.json | 6 ++ packages/runtime/src/scr_async.c | 100 +++++++++++++++++- packages/runtime/src/scr_async.h | 12 +++ packages/runtime/src/scr_async_dyn.c | 1 + packages/runtime/src/scr_test.c | 20 ++++ packages/runtime/src/scr_test.h | 6 ++ tests/corpus/5815-als-callback-context.ts | 76 +++++++++++++ tests/harness/corpus-costs.json | 2 + 17 files changed, 299 insertions(+), 30 deletions(-) create mode 100644 docs/content/limitations/builtin-apis/asynclocalstorage.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json create mode 100644 tests/corpus/5815-als-callback-context.ts diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index ae166836f..d261b6385 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -64,6 +64,8 @@ title: "Limitations" ../limitations/builtin-apis/eventemitter.mdx +../limitations/builtin-apis/asynclocalstorage.mdx + ../limitations/builtin-apis/streams-and-readline.mdx ../limitations/builtin-apis/commonjs-metadata.mdx diff --git a/docs/content/limitations/builtin-apis/asynclocalstorage.mdx b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx new file mode 100644 index 000000000..020382915 --- /dev/null +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -0,0 +1 @@ +- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node. Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started. diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 7d0fcf75c..a0d7bec4b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -880,7 +880,8 @@ "confidence": "high", "source": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -932,7 +933,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -959,7 +961,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -986,7 +989,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1013,7 +1017,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.run", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1040,7 +1045,8 @@ "confidence": "high", "source": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } @@ -1771,7 +1777,8 @@ "confidence": "high", "source": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c87f98fec..cfbde8940 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1953,7 +1953,8 @@ "status": "partial", "evidence": "derived:descendants", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "chapter" @@ -2002,7 +2003,8 @@ "status": "partial", "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2074,7 +2076,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.disable", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2100,7 +2103,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.getStore", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2126,7 +2130,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.enterWith", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2152,7 +2157,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.run", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2178,7 +2184,8 @@ "status": "partial", "evidence": "compiler-dedicated:asyncLocalStorage.exit", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" @@ -2481,6 +2488,7 @@ "evidence": "derived:descendants", "tests": [ "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts", "tests/harness/node-test.test.ts" ] }, @@ -3042,7 +3050,8 @@ "status": "partial", "evidence": "compiler-dedicated:AsyncLocalStorage", "tests": [ - "tests/corpus/2213-async-local-storage.cjs" + "tests/corpus/2213-async-local-storage.cjs", + "tests/corpus/5815-als-callback-context.ts" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 584492bf1..2e5eb1c4c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -36,7 +36,7 @@ "syntax.for-await-readable-streams": ["tests/corpus/2861-readable-iterator-close.ts", "tests/corpus/2862-web-readable-async-iteration.ts"], "syntax.for-await-class-async-iterators": ["tests/corpus/2863-class-async-iterator.ts"], "node-builtin.assert": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1609-assert-async.ts"], - "node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs"], + "node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"], "node-builtin.buffer": ["tests/corpus/1402-buffer-encodings.ts", "tests/corpus/1660-buffer-read-write-num.ts", "tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"], "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], @@ -395,7 +395,7 @@ { "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] }, { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] }, - { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] }, + { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] }, { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 588e8c26a..872153f40 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -968,6 +968,14 @@ export class LlEmitter { /** The program calls mock.timers members (moduleUsesMockTimers): timer * calls emit scr_test.c's mock-aware entry points. */ readonly mockTimers: boolean; + private schedulesInContextValue: boolean | undefined = undefined; + /** Whether timer and tick scheduling goes through the *_als entry + * points (RuntimeFeatures asyncContext). */ + get schedulesInContext(): boolean { + if (this.schedulesInContextValue === undefined) + this.schedulesInContextValue = moduleRuntimeFeatures(this.mod).asyncContext; + return this.schedulesInContextValue; + } /** Emitted ref-kind resolve thunks for new Promise, interned per inner * typeKey → thunk symbol. */ readonly resolveThunks = new Map(); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index f21ded106..034f0b6f1 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -526,7 +526,13 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) { throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); } - const sym = mockTimerSym(host, e) ?? LIB_FN_SYMS[e.fn]; + // queueMicrotask's checked-dynamic form carries the AsyncLocalStorage + // context like the static one (lib-process.ts). + const sym = + mockTimerSym(host, e) ?? + (e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext + ? "scr_queue_microtask_dyn_als" + : LIB_FN_SYMS[e.fn]); if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc); const args = READ_ONLY_STRING_LIB_FNS.has(e.fn) ? emitBorrowedInputs(host, e.args) diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 546c9a209..3c35ae6dc 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -374,16 +374,18 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr host.usesTimers = true; const cb = host.emitExpr(e.args[0]!); host.moveTemp(cb); + // Programs that enter AsyncLocalStorage stores schedule through the + // *_als entry points, which carry the active stores into the callback. const sym = - mockTimerSym(host, e) ?? - { - "timers.setTimeout": "scr_set_timeout", - "timers.setInterval": "scr_set_interval", - "timers.setTimeoutHandle": "scr_set_timeout_handle", - "timers.setImmediate": "scr_set_immediate", - "process.nextTick": "scr_next_tick", - "timers.queueMicrotask": "scr_queue_microtask", - }[e.fn]; + (mockTimerSym(host, e) ?? + { + "timers.setTimeout": "scr_set_timeout", + "timers.setInterval": "scr_set_interval", + "timers.setTimeoutHandle": "scr_set_timeout_handle", + "timers.setImmediate": "scr_set_immediate", + "process.nextTick": "scr_next_tick", + "timers.queueMicrotask": "scr_queue_microtask", + }[e.fn]!) + (host.schedulesInContext ? "_als" : ""); const rest = e.args.slice(1).map((a) => host.emitExpr(a)); const argList = [`ptr ${cb.name}`, ...rest.map((a) => `${host.llType(a.type)} ${a.name}`)].join( ", ", diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 04443ee18..d50d173b8 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -24,6 +24,11 @@ export interface RuntimeFeatures { assert: boolean; dynInvoke: boolean; dynAsync: boolean; + /** The program enters AsyncLocalStorage stores (store.run, enterWith, + * a diagnostics_channel store binding): its timer, immediate, tick and + * queueMicrotask scheduling goes through the runtime's *_als entry + * points, which carry the active stores into each callback. */ + asyncContext: boolean; inspect: boolean; childProcess: boolean; net: boolean; @@ -108,6 +113,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "dc.chanRunStores", ]); +/** The libCalls that enter AsyncLocalStorage stores (RuntimeFeatures + * asyncContext). */ +const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([ + "als.run", + "als.exitRun", + "als.enterWith", + "dc.chanBindStore", + "dc.chanRunStores", +]); + const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ "url.legacyParse", "url.legacyFormat", @@ -203,6 +218,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run dc: false, assert: false, dynInvoke: mod.workers === true, + asyncContext: false, dynAsync: mod.workers === true || mod.functions.some( @@ -360,6 +376,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run ) features.dynInvoke = true; if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; + if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true; if ( fn.startsWith("insp.") || fn.startsWith("console.") || diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json new file mode 100644 index 000000000..f192a6b22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5815-als-callback-context.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5815-als-callback-context.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index f630ee5f9..b1ebfc6e9 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -694,6 +694,26 @@ static inline void *scr_fiber_region_capture(void) { return regions != NULL ? regions->capture() : NULL; } +/* enterWith on the main stack lasts until that code returns to the loop, + * so the store does not reach callbacks scheduled without it: the loop + * clears the main slot when the main module's body returns, and an + * enterWith inside a callback the loop runs puts a tick that clears it at + * the front of the tick queue, so it runs as soon as that callback + * returns. */ +static SCR_TL bool scr_loop_started = false; + +static void scr_als_main_clear(void) { + scr_als_ctx_release(scr_als_main_slot); + scr_als_main_slot = NULL; +} + +static void scr_next_tick_raw_first(void (*fn)(void)); + +void scr_als_entered_with(void) { + if (scr_loop_started && SCR_ALS_SLOT() == &scr_als_main_slot) + scr_next_tick_raw_first(scr_als_main_clear); +} + static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */ static SCR_TL ScrFiber *scr_current = NULL; /* A finished fiber whose last switch went to the loop (scr_resume_fiber). */ @@ -1168,6 +1188,17 @@ void scr_next_tick_raw(void (*fn)(void)) { scr_nt_tail = t; } +/* A raw tick at the front of the queue: it runs before the ticks already + * queued. */ +static void scr_next_tick_raw_first(void (*fn)(void)) { + ScrNtick *t = calloc(1, sizeof *t); + if (!t) scr_oom(); + t->raw = fn; + t->next = scr_nt_head; + scr_nt_head = t; + if (scr_nt_tail == NULL) scr_nt_tail = t; +} + void scr_nticks_teardown(void) { while (scr_nt_head != NULL) { ScrNtick *t = scr_nt_head; @@ -3731,6 +3762,54 @@ void scr_queue_microtask(ScrClosure *cb) { scr_ready_push(f); } +/* ── scheduling in an AsyncLocalStorage context ─────────────────────── + * Programs that enter AsyncLocalStorage stores schedule their timer, + * interval, setImmediate, process.nextTick and queueMicrotask callbacks + * through these entry points (the emitter picks them when the program + * uses the store API), so each callback runs with the stores that were + * active when it was scheduled, as Node's async resources capture them. + * With a snapshot active, the callback is wrapped in a closure that + * installs it around the call; other programs keep the plain entry + * points. */ +static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); } +static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); } + +static void scr_als_bound_fire(ScrClosure *self) { + ScrClosure *cb = scr_box_get_ref(self->caps[0]); + ScrAlsCtx **slot = SCR_ALS_SLOT(); + ScrAlsCtx *prev = *slot; + *slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */ + ((void (*)(ScrClosure *))cb->fn)(cb); + /* An enterWith inside the callback replaced the slot's snapshot; either + * way the slot's reference is released here. */ + slot = SCR_ALS_SLOT(); + scr_als_ctx_release(*slot); + *slot = prev; + scr_closure_release(cb); +} + +static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/) { + ScrAlsCtx *active = *SCR_ALS_SLOT(); + if (active == NULL) return cb; + ScrClosure *bound = scr_closure_new((void *)scr_als_bound_fire, 2); + bound->caps[0] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bound->caps[0], cb); + bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL); + scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active)); + return bound; +} + +void scr_set_timeout_als(ScrClosure *cb, double ms) { scr_set_timeout(scr_als_bind(cb), ms); } +double scr_set_interval_als(ScrClosure *cb, double ms) { + return scr_set_interval(scr_als_bind(cb), ms); +} +double scr_set_timeout_handle_als(ScrClosure *cb, double ms) { + return scr_set_timeout_handle(scr_als_bind(cb), ms); +} +double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); } +void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); } +void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); } + /* The checked-dynamic argument form (JS files — common.mustCall wrappers * and the suite's invalid-input probes): a non-function throws Node's * ERR_INVALID_ARG_TYPE synchronously; a function value is called with @@ -3743,16 +3822,28 @@ static void scr_micro_dyn_fire(ScrClosure *c) { scr_dyn_release(fn); } -void scr_queue_microtask_dyn(const ScrDyn *cb) { +/* The closure that calls a dyn function value, or NULL with Node's + * ERR_INVALID_ARG_TYPE pending for a non-function. */ +static ScrClosure *scr_micro_dyn_closure(const ScrDyn *cb) { if (cb->kind != SCR_DYN_FUNC) { static const char msg[] = "The \"callback\" argument must be of type function."; scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE"); - return; + return NULL; } ScrClosure *c = scr_closure_new((void *)scr_micro_dyn_fire, 1); c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL); scr_box_set_ref(c->caps[0], scr_dyn_retain((ScrDyn *)cb)); - scr_queue_microtask(c); + return c; +} + +void scr_queue_microtask_dyn(const ScrDyn *cb) { + ScrClosure *c = scr_micro_dyn_closure(cb); + if (c) scr_queue_microtask(c); +} + +void scr_queue_microtask_dyn_als(const ScrDyn *cb) { + ScrClosure *c = scr_micro_dyn_closure(cb); + if (c) scr_queue_microtask_als(c); } /* ── the event hooks (scr_events.c) ────────────────────────────────── @@ -4520,6 +4611,9 @@ static SCR_TL bool scr_loop_cjs_entry = false; void scr_loop_commonjs_entry(void) { scr_loop_cjs_entry = true; } bool scr_loop_run(ScrPromise *top_level) { + /* The main module's body has returned: an enterWith it made ends here. */ + scr_loop_started = true; + if (scr_als_main_slot != NULL) scr_als_main_clear(); bool first_checkpoint = !scr_loop_cjs_entry; bool rejection_failed; for (;;) { diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index efa98e942..96421ca1c 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -158,6 +158,8 @@ ScrAlsCtx **scr_als_slot(void); #endif ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c); void scr_als_ctx_release(ScrAlsCtx *c); +/* After an enterWith: on the main stack, queues the tick that ends it. */ +void scr_als_entered_with(void); double scr_als_new(void); ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ @@ -409,6 +411,7 @@ double scr_set_immediate(ScrClosure *cb); /* cb ownership moves in */ * synchronously on a non-function; borrowed. */ void scr_queue_microtask(ScrClosure *cb); void scr_queue_microtask_dyn(const ScrDyn *cb); +void scr_queue_microtask_dyn_als(const ScrDyn *cb); /* setImmediate AS A dyn VALUE (the traceCallback shape): a minted dyn * callable that schedules args[0](args[1..]) on the immediate queue and * answers undefined; a non-function first argument throws Node's @@ -428,6 +431,15 @@ bool scr_immediate_has_ref(double handle); * teardown (they must never run yet must not leak). cb ownership moves * in. */ void scr_next_tick(ScrClosure *cb); +/* The scheduling entry points of programs that enter AsyncLocalStorage + * stores: each callback runs with the stores active when it was scheduled + * (scr_async.c). */ +void scr_set_timeout_als(ScrClosure *cb, double ms); +double scr_set_interval_als(ScrClosure *cb, double ms); +double scr_set_timeout_handle_als(ScrClosure *cb, double ms); +double scr_set_immediate_als(ScrClosure *cb); +void scr_next_tick_als(ScrClosure *cb); +void scr_queue_microtask_als(ScrClosure *cb); /* A successful process.stdout/stderr.write completion on that same queue. * The callback and its program-shaped Error | null adapter both MOVE into * the entry; the adapter is invoked with NULL (Node's success argument). */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index ffcb85050..7611378fb 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -120,6 +120,7 @@ void scr_als_restore(ScrAlsCtx *prev) { void scr_als_enter_with(double id, ScrDyn *value) { ScrAlsCtx *prev = scr_als_enter(id, value); scr_als_ctx_release(prev); /* no restore point — Node's enterWith */ + scr_als_entered_with(); } void scr_als_disable(double id) { diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 00e609129..43affdd8a 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -2279,6 +2279,26 @@ double scr_test_mt_set_immediate(ScrClosure *cb /*moves*/) { ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; return m ? scr_mt_create(m, false, cb, -1)->handle : scr_set_immediate(cb); } +/* The same calls in a program that enters AsyncLocalStorage stores: a + * mocked timer runs in the context its tick() runs in, as in Node; a real + * one keeps the stores active when it was scheduled. */ +void scr_test_mt_set_timeout_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + if (m) (void)scr_mt_create(m, false, cb, ms); + else scr_set_timeout_als(cb, ms); +} +double scr_test_mt_set_timeout_handle_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_TIMEOUT]; + return m ? scr_mt_create(m, false, cb, ms)->handle : scr_set_timeout_handle_als(cb, ms); +} +double scr_test_mt_set_interval_als(ScrClosure *cb /*moves*/, double ms) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_INTERVAL]; + return m ? scr_mt_create(m, true, cb, ms)->handle : scr_set_interval_als(cb, ms); +} +double scr_test_mt_set_immediate_als(ScrClosure *cb /*moves*/) { + ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; + return m ? scr_mt_create(m, false, cb, -1)->handle : scr_set_immediate_als(cb); +} void scr_test_mt_clear_immediate(double handle) { ScrMockTimers *m = scr_mt_owner[SCR_MT_IMMEDIATE]; if (m) scr_mt_clear(m, handle); diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index 64a280bf7..b29fca362 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -110,6 +110,12 @@ void scr_test_mt_clear_immediate(double handle); void scr_test_mt_immediate_unref(double handle); void scr_test_mt_immediate_ref(double handle); bool scr_test_mt_immediate_has_ref(double handle); +/* The setters of programs that also enter AsyncLocalStorage stores: an + * unmocked call uses the loop's *_als function. */ +void scr_test_mt_set_timeout_als(ScrClosure *cb, double ms); +double scr_test_mt_set_timeout_handle_als(ScrClosure *cb, double ms); +double scr_test_mt_set_interval_als(ScrClosure *cb, double ms); +double scr_test_mt_set_immediate_als(ScrClosure *cb); double scr_test_mt_date_now(void); /* timers/promises scheduler.wait(delay) (+1 promise; NULL after a throw). */ ScrPromise *scr_test_mt_scheduler_wait(double ms); diff --git a/tests/corpus/5815-als-callback-context.ts b/tests/corpus/5815-als-callback-context.ts new file mode 100644 index 000000000..461a2aa95 --- /dev/null +++ b/tests/corpus/5815-als-callback-context.ts @@ -0,0 +1,76 @@ +// AsyncLocalStorage context reaches timer, interval, setImmediate, +// process.nextTick and queueMicrotask callbacks: each runs in the store +// that was active when it was scheduled, including callbacks they schedule +// in turn, an interval on every tick, and a refreshed timer. A cleared +// timer or immediate never runs. enterWith inside a callback lasts until +// the callback returns, also in a callback scheduled outside any store, +// and callbacks scheduled outside any store, or inside exit(), see +// undefined. A store entered at the top level reaches only the callbacks +// scheduled after it. +import { AsyncLocalStorage } from "node:async_hooks"; + +const requests = new AsyncLocalStorage(); +const users = new AsyncLocalStorage(); +const seen: string[] = []; + +function record(where: string): void { + seen.push(`${where}: ${requests.getStore() ?? "-"} ${users.getStore() ?? "-"}`); +} + +requests.run("req-1", () => { + setTimeout(() => { + record("timeout"); + process.nextTick(() => record("tick from timeout")); + queueMicrotask(() => record("microtask from timeout")); + }, 1); + setImmediate(() => { + record("immediate"); + setTimeout(() => record("timeout from immediate"), 1); + }); + process.nextTick(() => record("tick")); + queueMicrotask(() => record("microtask")); + users.run("ada", () => { + setTimeout(() => record("nested timeout"), 2); + setImmediate(() => record("nested immediate")); + }); + let ticks = 0; + const interval = setInterval(() => { + ticks++; + record(`interval ${ticks}`); + if (ticks === 3) clearInterval(interval); + }, 1); + const refreshed = setTimeout(() => record("refreshed timeout"), 3); + refreshed.refresh(); + const cleared = setTimeout(() => record("cleared timeout"), 1); + clearTimeout(cleared); + const clearedImmediate = setImmediate(() => record("cleared immediate")); + clearImmediate(clearedImmediate); + requests.exit(() => { + setTimeout(() => record("timeout in exit"), 1); + queueMicrotask(() => record("microtask in exit")); + }); +}); + +requests.run("req-2", () => { + setTimeout(() => { + users.enterWith("grace"); + record("timeout after enterWith"); + setTimeout(() => record("timeout from enterWith"), 1); + }, 4); + setTimeout(() => record("timeout after the enterWith timeout"), 5); +}); + +setTimeout(() => { + record("timeout outside any store"); + requests.enterWith("entered outside"); +}, 1); +setTimeout(() => record("timeout after one that entered outside any store"), 8); +process.nextTick(() => record("tick outside any store")); + +setTimeout(() => { + record("final"); + for (const line of seen.sort()) console.log(line); +}, 30); + +users.enterWith("top level"); +setTimeout(() => record("timeout after a top-level enterWith"), 6); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 1a4ad1180..a03ce551f 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2092,6 +2092,7 @@ "5812-dyn-box-async-fields.ts": 3522, "5813-mock-timers-module.ts": 3766, "5814-mock-timers-date.ts": 3760, + "5815-als-callback-context.ts": 3110, "5830-timers-promises-signal.ts": 3875, "5831-util-types-brands.ts": 3347, "5832-util-types-brands-dynamic.js": 3250, @@ -5165,6 +5166,7 @@ "5812-dyn-box-async-fields.ts": 5278, "5813-mock-timers-module.ts": 4227, "5814-mock-timers-date.ts": 17708, + "5815-als-callback-context.ts": 4542, "5830-timers-promises-signal.ts": 4170, "5831-util-types-brands.ts": 4380, "5832-util-types-brands-dynamic.js": 3658, From a1a475c76f21f5338793ada72d2afe5f36fedb15 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 22:30:45 -0500 Subject: [PATCH 51/67] Count each statement once in coverage however often it lowers Speculative lowering lowers some function bodies several times: the receiver of `(function () { ... }).call(this)` goes through four call lowerings that each probe it with tryLowerExpression, and each probe lowered and counted the body. A five-statement program with such a wrapper reported 14 statements, and a fenced statement inside it counted as four failures. The Lowerer now records which statements and class field initializers are in the totals and which are counted as failed, and counts each node once. Corpus 6110's @deferred-fences directive stays 1, its one fenced statement in a class base lowered once, and its header no longer ties the count to how many times that body lowers. --- .../src/frontend/lowering/lower-classes.ts | 12 +++------ .../src/frontend/lowering/lower-stmts.ts | 10 ++----- .../compiler/src/frontend/lowering/lowerer.ts | 26 +++++++++++++++++++ .../6110-cjs-speculative-wrapper-fences.cjs | 3 +-- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a4b771ded..d44748084 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -4598,8 +4598,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] continue; } const f = item.f; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -4635,8 +4634,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] out.push({ kind: "assign", localId: f.globalId, value, loc: locOf(f.initializer) }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null @@ -7764,8 +7762,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } if (!f.initializer) continue; - lowerer.stats.statementsTotal++; - lowerer.bumpFileStat(locOf(f.initializer).file, "total"); + lowerer.countStatement(f.initializer, locOf(f.initializer).file); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -7780,8 +7777,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL }); } catch (e) { if (!(e instanceof PoisonError)) throw e; - lowerer.stats.statementsFailed++; - lowerer.bumpFileStat(locOf(f.initializer).file, "failed"); + lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); if (isJsSourceFile(f.initializer.getSourceFile())) { const fence = lowerer.diagSink === null diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 43ad3819b..8ded9106b 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -689,10 +689,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir entry.index = i; if (unreachable && skipsWhenUnreachable(stmt)) continue; if (inFunction && isAbruptStatement(stmt)) unreachable = true; - if (!lowerer.suppressStats) { - lowerer.stats.statementsTotal++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total"); - } + if (!lowerer.suppressStats) lowerer.countStatement(stmt, sf?.fileName); const diagsBefore = lowerer.diags.length; const sinkMark = lowerer.diagSinkMark(); try { @@ -757,10 +754,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir } else if (!(e instanceof PoisonError)) { throw e; } - if (!lowerer.suppressStats) { - lowerer.stats.statementsFailed++; - if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "failed"); - } + if (!lowerer.suppressStats) lowerer.countStatementFailed(stmt, sf?.fileName); lowerer.noteBlockedBindings(stmt); // The runtime-fence conversion (JS sources, direct diagnostics // only — a capture wrapper in force means collection owns the diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6493bc5f3..a77142cb0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2925,6 +2925,32 @@ export class Lowerer { * pass skips it (attribution describes the build). */ readonly statsByFile = new Map(); + /** Statements and field initializers already in the coverage totals, and + * those already counted as failed. Speculative lowering can lower one + * function body several times: each receiver probe of + * `(function () { ... }).call(this)` lowers it. A source statement counts + * once however many times it lowers. */ + private readonly countedStatements = new Set(); + private readonly failedStatements = new Set(); + + /** Adds `node` to the coverage totals unless it is already counted (the + * caller checks suppressStats). */ + countStatement(node: ts.Node, file: string | undefined): void { + if (this.countedStatements.has(node)) return; + this.countedStatements.add(node); + this.stats.statementsTotal++; + if (file !== undefined) this.bumpFileStat(file, "total"); + } + + /** Counts `node` as failed unless it already is (the caller checks + * suppressStats). */ + countStatementFailed(node: ts.Node, file: string | undefined): void { + if (this.failedStatements.has(node)) return; + this.failedStatements.add(node); + this.stats.statementsFailed++; + if (file !== undefined) this.bumpFileStat(file, "failed"); + } + /** Bumps the per-file attribution counter (no-op unless provenance is * active and this is the emit/discovery lane — mirror the CALLER's * suppressStats guard, this method only gates remainder). */ diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index fbb1a65fc..3ae9df3dc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -5,8 +5,7 @@ // and `typeof module.exports` compile; a statement with no static lowering // (here `module.exports` passed as a value in the class base) compiles to // its runtime fence instead of vanishing. It never runs, so the binary -// matches Node. Coverage counts that one fenced statement once: the class -// base's body is lowered once. +// matches Node. const verbose = process.argv.length > 99; (function () { var root = this; From 05f2dd585987519c4bc728d941535bf7df6f12ab Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:03:25 -0500 Subject: [PATCH 52/67] Walk Map and Set iterators inside iterator helper objects A helper object over a Map or Set iterator that is stored and consumed later (const it = set.values().map(f); for (const x of it) ...) stepped the runtime's checked iterator through an adapter generator, boxing each value and resuming two generators per element: 5.1x Node's time on a 100k-element Set. A stage on m.keys(), m.values(), m.entries() (or a Set's), or on Iterator.from(set) or Iterator.from(map), now walks the collection inside the helper's own generator. The walk steps the runtime's native iterator by entry index through the new scr_dyn_collection_iterator_index and reads each key or value with the typed intrinsics. The native iterator keeps the walk live and ends the collection's iteration when it is exhausted or released, so an abandoned or returned helper leaves the collection free to compact. Other helper sources over a Map or Set, and Iterator.from over a Set or Map, use a typed generator over the collection built the same way. --- .../backend/llvm/checked-value-lifetimes.ts | 7 +- .../compiler/src/backend/llvm/lib-shared.ts | 1 + .../lowering/containers/iterator-helpers.ts | 233 +++++++++++++----- .../frontend/lowering/iterator-adapters.ts | 116 +++++++++ .../compiler/src/ir/builtin-signatures.ts | 1 + packages/compiler/src/ir/int32-slots.ts | 1 + .../6073-stored-collection-helpers.ts.json | 6 + .../6074-stored-collection-helpers-js.js.json | 6 + packages/runtime/src/scr_dyn.h | 4 + packages/runtime/src/scr_json.c | 15 ++ .../corpus/6073-stored-collection-helpers.ts | 60 +++++ .../6074-stored-collection-helpers-js.js | 57 +++++ tests/harness/corpus-costs.json | 4 + 13 files changed, 453 insertions(+), 58 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json create mode 100644 tests/corpus/6073-stored-collection-helpers.ts create mode 100644 tests/corpus/6074-stored-collection-helpers-js.js diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index c359f6175..5f8119079 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -42,5 +42,10 @@ export function borrowsJsonInputs(fn: string): boolean { * loop. Runtime steps borrow that state but may invoke indexed getters, so * this permits stable-local borrowing without preserving reference edges. */ export function borrowsIteratorInputs(fn: string): boolean { - return fn === "dyn.iteratorCanStep" || fn === "dyn.iteratorStep" || fn === "dyn.iteratorStepDone"; + return ( + fn === "dyn.iteratorCanStep" || + fn === "dyn.iteratorStep" || + fn === "dyn.iteratorStepDone" || + fn === "dyn.collectionIteratorIndex" + ); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index ef83bf07e..8d758a0db 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -889,6 +889,7 @@ export const LIB_FN_SYMS: Record = { "dyn.iteratorCanStep": "scr_dyn_iterator_can_step", "dyn.iteratorStep": "scr_dyn_iterator_step", "dyn.iteratorStepDone": "scr_dyn_iterator_step_done", + "dyn.collectionIteratorIndex": "scr_dyn_collection_iterator_index", "dyn.mapSeedEntries": "scr_dyn_map_seed_entries", "dyn.mapSeedEntry": "scr_dyn_map_seed_entry", "dyn.arrLen": "scr_dyn_arr_len", diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 70e0d85e0..229999627 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -39,6 +39,7 @@ import { iteratorSlotAdapter, lowerArrayIteratorValue, lowerBytesIteratorValue, + lowerCollectionIteratorValue, lowerStringIteratorValue, } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; @@ -157,15 +158,45 @@ export function lowerGeneratorHelperCall( if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); } + // A stage on m.keys()/values()/entries() (or a Set's) walks the + // collection inside the helper's own generator. + const collection = collectionIteratorCall(lowerer, access.expression); + if (collection !== null) { + const coll = lowerer.lowerExpr(collection.receiver); + if (coll.type.kind !== "map" && coll.type.kind !== "set") + lowerer.badType(collection.receiver, lowerer.typeOf(collection.receiver)); + const source: ChainSource = { kind: "collection", type: coll.type, proj: collection.proj }; + const elem = collectionSeed(lowerer, source); + const { stage, out } = lowerStage(lowerer, { name, argNode, access }, elem, loc); + return lowerHelperObject(lowerer, call, coll, source, elem, stage, out, loc); + } const src = lowerHelperSource(lowerer, access.expression); if (name !== "take" && name !== "drop") { const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc); - return lowerHelperObject(lowerer, call, src.gen, src.elem, stage, out, loc); + return lowerHelperObject( + lowerer, + call, + src.gen, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.elem, + stage, + out, + loc, + ); } // The budget check closes the source, so the source is bound first. const bound = bindSource(lowerer, src.gen, loc); const { stage, out } = lowerStage(lowerer, { name, argNode, access }, src.elem, loc, bound.ref); - const helper = lowerHelperObject(lowerer, call, bound.ref, src.elem, stage, out, loc); + const helper = lowerHelperObject( + lowerer, + call, + bound.ref, + { kind: "generator", type: src.gen.type, elem: src.elem }, + src.elem, + stage, + out, + loc, + ); return { kind: "seqExpr", stmts: [bound.decl], result: helper, type: helper.type, loc }; } // The stages before the terminal, back to the first receiver that is @@ -281,8 +312,8 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { * iterator, which already inherits from Iterator.prototype, so Node returns * it rather than a wrapper. A generator (including a helper object or an * array iterator) is itself; an array, string or typed array gives its - * values iterator; a Set its values and a Map its entries, adapted into a - * generator over the live collection. The result converts into the call's + * values iterator; a Set its values and a Map its entries, as a generator + * over the live collection. The result converts into the call's * IteratorObject type. Null for other arguments (objects with their own * next method, untyped values), which keep the Iterator fence. */ export function lowerIteratorFromCall( @@ -317,16 +348,13 @@ export function lowerIteratorFromCall( iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc); else if (t.kind === "set" || t.kind === "map") { const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value); - const native: IrExpr = { - kind: "dynInvoke", - recv: lowerer.coerceToExpected(value, DYN), - method: t.kind === "set" ? "values" : "entries", - calleeName: t.kind === "set" ? "set.values" : "map.entries", - args: [], - type: DYN, + iterator = lowerCollectionIteratorValue( + lowerer, + value, + t.kind === "set" ? "values" : "entries", + own(elem), loc, - }; - iterator = iteratorSlotAdapter(lowerer, native, own(elem)); + ); } if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode)); const result = lowerer.coerceToExpected(iterator, resultT); @@ -686,6 +714,14 @@ function lowerHelperSource( ) lowerer.badType(expr, tsType); const target: GenType = { kind: "generator", yieldT, retT: VOID, nextT: UNDEFINED_T }; + // m.keys()/values()/entries() (or a Set's) on a typed collection: a + // generator over the collection with typed reads. + const collection = collectionIteratorCall(lowerer, expr); + if (collection !== null) { + const coll = lowerer.lowerExpr(collection.receiver); + const walk = lowerCollectionIteratorValue(lowerer, coll, collection.proj, target, locOf(expr)); + if (walk !== null) return { gen: walk as IrExpr & { type: GenType }, elem: yieldT }; + } const adapted = iteratorSlotAdapter(lowerer, lowerer.lowerExpr(expr), target); if (adapted === null || adapted.type.kind !== "generator") lowerer.badType(expr, tsType); return { gen: adapted as IrExpr & { type: GenType }, elem: yieldT }; @@ -937,7 +973,8 @@ function lowerFusedChain( function lowerHelperObject( lowerer: Lowerer, call: ts.CallExpression, - src: IrExpr & { type: GenType }, + src: IrExpr, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, out: IrType, @@ -961,21 +998,23 @@ function lowerHelperObject( "the helper's element type must be representable — annotate the callback's types", ); } - if (!stepDrivable(src.type)) { + if (source.kind === "generator" && !stepDrivable(source.type)) { lowerer.noLowering( - `.${stage.kind} over an iterator yielding '${lowerer.fmt(src.type.yieldT)}'`, + `.${stage.kind} over an iterator yielding '${lowerer.fmt(source.type.yieldT)}'`, call, "helpers over iterators of numbers, booleans, strings, arrays, records and class instances are the supported forms", ); } const arg = "budget" in stage ? stage.budget : stage.fn; - const key = `iterHelper:${stage.kind}:${typeKey(src.type)}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; + const srcKey = + source.kind === "collection" ? `${source.proj}:${typeKey(source.type)}` : typeKey(source.type); + const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( - buildHelperGenFn(lowerer, name, src.type, srcElem, stage, arg.type, outT, resultType, loc), + buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), ); } return { kind: "call", callee: name, args: [src, arg], type: outT, loc }; @@ -1267,6 +1306,107 @@ function driveGenerator( ]; } +/** Statements that walk the Map or Set in local `collId` from inside a + * helper's generator: each pass runs `pre` (which may `break`), takes the + * next live entry index from the runtime's native iterator, which keeps + * the walk live and ends it when exhausted or released, and runs `body` + * over the projected element read with the typed intrinsics. */ +function driveCollectionNative( + lowerer: Lowerer, + frame: ChainFrame, + collId: string, + source: ChainSource & { kind: "collection" }, + pre: IrStmt[], + body: (value: IrExpr) => IrStmt[], + loc: SrcLoc, +): IrStmt[] { + const t = source.type; + const coll = (): IrExpr => varRef(collId, t, loc); + const nativeId = frame.local("native", DYN, false); + const iId = frame.local("i", F64, false); + const read = (method: "iterKey" | "iterValue", type: IrType): IrExpr => + t.kind === "map" + ? { kind: "mapIntrinsic", method, receiver: coll(), args: [varRef(iId, F64, loc)], type, loc } + : { + kind: "setIntrinsic", + method: "iterKey", + receiver: coll(), + args: [varRef(iId, F64, loc)], + type, + loc, + }; + const keyT = t.kind === "map" ? t.key : t.elem; + const valueT = t.kind === "map" ? t.value : t.elem; + const seedT = collectionSeed(lowerer, source); + const seed: IrExpr = + source.proj === "entries" + ? { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: seedT, + loc, + } + : source.proj === "values" && t.kind === "map" + ? read("iterValue", valueT) + : read("iterKey", keyT); + const vId = frame.local("v", seedT, false); + return [ + { + kind: "varDecl", + localId: nativeId, + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll(), DYN), + method: "values", + calleeName: t.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "while", + cond: boolLit(true, loc), + body: [ + ...pre, + { + kind: "varDecl", + localId: iId, + init: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [varRef(nativeId, DYN, loc)], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: varRef(iId, F64, loc), + right: numLit(0, loc), + type: BOOL, + loc, + }, + then: [{ kind: "break", loc }], + else_: null, + loc, + }, + { kind: "varDecl", localId: vId, init: seed, loc }, + ...body(varRef(vId, seedT, loc)), + ], + loc, + }, + ]; +} + /** `for (j = 0; j < arr.length && !stop; j++) body(arr[j])` over a live * array local (the array iterator's per-step length read). */ function walkArray( @@ -1978,7 +2118,7 @@ function buildIterChainFn( function buildHelperGenFn( lowerer: Lowerer, name: string, - srcT: GenType, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, argT: IrType, @@ -1987,8 +2127,12 @@ function buildHelperGenFn( loc: SrcLoc, ): IrFunction { const frame = new LiftedFrame(loc); - const srcId = frame.param("src", srcT); + const srcId = frame.param("src", source.type); const argId = frame.param(stage.kind === "take" || stage.kind === "drop" ? "n" : "f", argT); + const drive = (pre: IrStmt[], each: (value: IrExpr) => IrStmt[], at: SrcLoc): IrStmt[] => + source.kind === "generator" + ? driveGenerator(lowerer, frame, srcId, source.type, srcElem, pre, each, at) + : driveCollectionNative(lowerer, frame, srcId, source, pre, each, at); const yieldOf = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { @@ -2014,12 +2158,7 @@ function buildHelperGenFn( const n = (): IrExpr => varRef(argId, F64, loc); if (stage.kind === "map") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const mId = frame.local("m", fnT!.ret, false); @@ -2034,12 +2173,7 @@ function buildHelperGenFn( ); } else if (stage.kind === "filter") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const okId = frame.local("ok", BOOL, false); @@ -2070,28 +2204,12 @@ function buildHelperGenFn( loc, }, ]; - body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, - pre, - (value) => [yieldOf(value)], - loc, - ), - ); + body.push(...drive(pre, (value) => [yieldOf(value)], loc)); } else if (stage.kind === "drop") { const droppedId = frame.local("dropped", F64, true); body.push({ kind: "varDecl", localId: droppedId, init: numLit(0, loc), loc }); body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => [ { @@ -2115,12 +2233,7 @@ function buildHelperGenFn( ); } else { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const innerT = fnT!.ret; @@ -2142,7 +2255,13 @@ function buildHelperGenFn( returnType: VOID, locals: frame.locals, body, - generator: { yieldT: outT.yieldT, nextT: outT.nextT, resultType, closesSource: true }, + generator: { + yieldT: outT.yieldT, + nextT: outT.nextT, + resultType, + // A native Map or Set iterator has no return() to call. + ...(source.kind === "generator" ? { closesSource: true as const } : {}), + }, loc, }; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 812f29242..f086417de 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -265,6 +265,122 @@ function dynIteratorAdapter( return { kind: "call", callee: name, args: [expr], type: expected, loc }; } +/** `m.keys()`, `m.values()` or `m.entries()` (or a Set's) as a source + * generator for an iterator helper object: an interned generator over the + * typed collection that steps the runtime's native iterator by entry + * index and reads each key or value with the typed intrinsics, so values + * keep their types without a checked conversion per step. The native + * iterator owns the live-walk bracket: it ends the iteration when the + * collection is exhausted or when the generator, done or abandoned, + * releases it. `type` yields the key, the value, or the interned [key, + * value] pair. Null when `type` does not match. */ +export function lowerCollectionIteratorValue( + lowerer: Lowerer, + receiver: IrExpr, + method: "keys" | "values" | "entries", + type: IrType, + loc: SrcLoc, +): IrExpr | null { + const collT = receiver.type; + if ((collT.kind !== "map" && collT.kind !== "set") || type.kind !== "generator") return null; + if (type.retT.kind !== "void" || type.nextT.kind !== "undefinedT") return null; + const keyT = collT.kind === "map" ? collT.key : collT.elem; + const valueT = collT.kind === "map" ? collT.value : collT.elem; + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `collIter:${method}:${typeKey(collT)}:${typeKey(type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const coll = varRef("coll.0", collT, loc); + const index = varRef("index.0", F64, loc); + const native = varRef("native.0", DYN, loc); + const read = (m: "iterKey" | "iterValue", t: IrType): IrExpr => + collT.kind === "map" + ? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc } + : { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc }; + let yielded: IrExpr; + if (method === "keys" || (collT.kind === "set" && method === "values")) + yielded = read("iterKey", keyT); + else if (method === "values") yielded = read("iterValue", valueT); + else + yielded = { + kind: "recordLit", + fields: [ + { name: "0", value: read("iterKey", keyT) }, + { name: "1", value: read("iterValue", valueT) }, + ], + type: type.yieldT, + loc, + }; + if (!typeEquals(yielded.type, type.yieldT)) return null; + name = `%coll.iter.${lowerer.valueHelpers.size}`; + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "coll.0", name: "coll", type: collT }], + returnType: VOID, + locals: [ + { id: "coll.0", name: "coll", type: collT, mutable: false }, + { id: "native.0", name: "native", type: DYN, mutable: false }, + { id: "index.0", name: "index", type: F64, mutable: true }, + ], + body: [ + { + kind: "varDecl", + localId: "native.0", + init: { + kind: "dynInvoke", + recv: lowerer.coerceToExpected(coll, DYN), + method: "values", + calleeName: collT.kind === "map" ? "map.values" : "set.values", + args: [], + type: DYN, + loc, + }, + loc, + }, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "varDecl", + localId: "index.0", + init: { + kind: "libCall", + fn: "dyn.collectionIteratorIndex", + args: [native], + type: F64, + loc, + }, + loc, + }, + { + kind: "if", + cond: { + kind: "bin", + op: "<", + left: index, + right: { kind: "numLit", value: 0, type: F64, loc }, + type: BOOL, + loc, + }, + then: [{ kind: "return", value: null, loc }], + else_: null, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value: yielded, type: VOID, loc }, loc }, + ], + loc, + }, + ], + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + loc, + }); + } + return { kind: "call", callee: name, args: [receiver], type, loc }; +} + /** `ta.values()`, `ta.keys()` or `ta.entries()` on a typed array as a * value: an interned generator over the indices, reading the length and * the element at each step, as the typed array iterator does. Null when diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51683990b..51765286b 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -182,6 +182,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.collectionIteratorIndex": { argTypes: [DYN], result: F64 }, "dyn.mapSeedEntries": { argTypes: [DYN], result: DYN }, "dyn.mapSeedEntry": { argTypes: [DYN], result: DYN }, /** The for-of-over-dyn pack accessors — the emitted index loop drives diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index d7d06db7d..903823358 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -371,6 +371,7 @@ const DYN_READ_ONLY = new Set([ "dyn.iteratorResult", "dyn.iteratorStep", "dyn.iteratorStepDone", + "dyn.collectionIteratorIndex", "dyn.mapSeedEntries", "dyn.mapSeedEntry", "dyn.numberConstructor", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json new file mode 100644 index 000000000..6d9eeff70 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6073-stored-collection-helpers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6073-stored-collection-helpers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json new file mode 100644 index 000000000..fbbaf0df4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6074-stored-collection-helpers-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6074-stored-collection-helpers-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 2606e1473..e76563ab7 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -702,6 +702,10 @@ ScrDyn *scr_dyn_iterator_result(ScrDyn *value); bool scr_dyn_iterator_can_step(const ScrDyn *iterator, const ScrDyn *next); ScrDyn *scr_dyn_iterator_step(const ScrDyn *iterator); bool scr_dyn_iterator_step_done(const ScrDyn *iterator); +/* A native Map or Set iterator stepped by the compiler's typed walk: the + * next live entry index of its collection, or -1 once the collection is + * exhausted (the iterator then ends its iteration). */ +double scr_dyn_collection_iterator_index(const ScrDyn *iterator); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0376ece2c..d18b3d910 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4673,6 +4673,21 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) { return ((ScrNativeIterator *)value->v.handle.ptr)->step_done; } +double scr_dyn_collection_iterator_index(const ScrDyn *value) { + ScrNativeIterator *iterator = value->v.handle.ptr; + ScrDyn *source = iterator->source; + if (!source) return -1; + ScrMap *map = source->v.handle.ptr; + iterator->index = scr_map_iter_skip(map, iterator->index); + if (iterator->index >= scr_map_iter_count(map)) { + iterator->source = NULL; + scr_map_iter_exit(map); + scr_dyn_release(source); + return -1; + } + return iterator->index++; +} + static int scr_ih_method_index(const char *name, size_t len); static ScrDyn *scr_iterator_helper_method(ScrDyn *self, int index, ScrDyn *const *args, size_t argc); static ScrDyn *scr_ih_method_function(int index); diff --git a/tests/corpus/6073-stored-collection-helpers.ts b/tests/corpus/6073-stored-collection-helpers.ts new file mode 100644 index 000000000..ed739eb85 --- /dev/null +++ b/tests/corpus/6073-stored-collection-helpers.ts @@ -0,0 +1,60 @@ +// Iterator helper objects over Map and Set iterators that are stored and +// consumed later: the helpers observe the collection live, and abandoned +// or returned helpers leave it usable. +const set = new Set([1, 2, 3]); +const doubled = set.values().map((x) => x * 2); +set.add(4); +set.delete(1); +console.log(doubled.next(), [...doubled]); + +const map = new Map([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const large = map.entries().filter(([, v]) => v > 1); +map.set("d", 4); +map.delete("b"); +console.log([...large].map(([k, v]) => `${k}=${v}`).join(" ")); +const keys = map.keys().map((k, i) => k.toUpperCase() + i); +console.log(keys.next(), keys.return!(), keys.next()); +const rest = map.values().drop(2); +for (const v of rest) console.log("value", v); +const firstTwo = map.keys().take(2); +console.log([...firstTwo], [...firstTwo]); + +// A stage on a stage, and Iterator.from over a Set and a Map. +const chained = set.values().filter((x) => x % 2 === 0).map((x) => `n${x}`); +console.log(chained.toArray()); +const fromSet = Iterator.from(set).map((x) => x + 0.5); +set.add(9); +console.log([...fromSet]); +console.log(Iterator.from(map).map(([k, v]) => k + v).toArray()); + +// A helper left mid-walk does not stop later changes or walks. +const abandoned = set.values().map((x) => x); +abandoned.next(); +for (let i = 0; i < 100; i++) { + set.add(100 + i); + set.delete(100 + i); +} +console.log(set.size, set.values().take(3).toArray(), [...set].length); +const returned = map.values().map((v) => v * 10); +returned.next(); +returned.return!(); +for (let i = 0; i < 50; i++) map.set(`t${i}`, i); +for (let i = 0; i < 50; i++) map.delete(`t${i}`); +console.log(map.size, [...map.keys()].join(",")); + +// Records and strings as elements. +interface Row { + id: number; + tag: string; +} +const rows = new Map([ + [1, { id: 1, tag: "x" }], + [2, { id: 2, tag: "y" }], +]); +const tags = rows.values().map((r) => r.tag.repeat(r.id)); +rows.set(3, { id: 3, tag: "z" }); +console.log([...tags], rows.entries().find(([, r]) => r.tag === "y")?.[0]); diff --git a/tests/corpus/6074-stored-collection-helpers-js.js b/tests/corpus/6074-stored-collection-helpers-js.js new file mode 100644 index 000000000..3a314a3c1 --- /dev/null +++ b/tests/corpus/6074-stored-collection-helpers-js.js @@ -0,0 +1,57 @@ +// The JavaScript form: iterator helper objects over Map and Set iterators +// that are stored and consumed later: the helpers observe the collection live, and abandoned +// or returned helpers leave it usable. +const set = new Set([1, 2, 3]); +const doubled = set.values().map((x) => x * 2); +set.add(4); +set.delete(1); +console.log(doubled.next(), [...doubled]); + +const map = new Map ([ + ["a", 1], + ["b", 2], + ["c", 3], +]); +const large = map.entries().filter(([, v]) => v > 1); +map.set("d", 4); +map.delete("b"); +console.log([...large].map(([k, v]) => `${k}=${v}`).join(" ")); +const keys = map.keys().map((k, i) => k.toUpperCase() + i); +console.log(keys.next(), keys.return(), keys.next()); +const rest = map.values().drop(2); +for (const v of rest) console.log("value", v); +const firstTwo = map.keys().take(2); +console.log([...firstTwo], [...firstTwo]); + +// A stage on a stage, and Iterator.from over a Set and a Map. +const chained = set.values().filter((x) => x % 2 === 0).map((x) => `n${x}`); +console.log(chained.toArray()); +const fromSet = Iterator.from(set).map((x) => x + 0.5); +set.add(9); +console.log([...fromSet]); +console.log(Iterator.from(map).map(([k, v]) => k + v).toArray()); + +// A helper left mid-walk does not stop later changes or walks. +const abandoned = set.values().map((x) => x); +abandoned.next(); +for (let i = 0; i < 100; i++) { + set.add(100 + i); + set.delete(100 + i); +} +console.log(set.size, set.values().take(3).toArray(), [...set].length); +const returned = map.values().map((v) => v * 10); +returned.next(); +returned.return(); +for (let i = 0; i < 50; i++) map.set(`t${i}`, i); +for (let i = 0; i < 50; i++) map.delete(`t${i}`); +console.log(map.size, [...map.keys()].join(",")); + +// Records and strings as elements. + +const rows = new Map ([ + [1, { id: 1, tag: "x" }], + [2, { id: 2, tag: "y" }], +]); +const tags = rows.values().map((r) => r.tag.repeat(r.id)); +rows.set(3, { id: 3, tag: "z" }); +console.log([...tags], rows.entries().find(([, r]) => r.tag === "y")?.[0]); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 98055a9f1..1793158d5 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2239,6 +2239,8 @@ "6070-iterator-helpers-untyped.js": 4050, "6071-iterator-sources.ts": 5150, "6072-iterator-sources-js.js": 5350, + "6073-stored-collection-helpers.ts": 4000, + "6074-stored-collection-helpers-js.js": 4100, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5314,6 +5316,8 @@ "6070-iterator-helpers-untyped.js": 6900, "6071-iterator-sources.ts": 5150, "6072-iterator-sources-js.js": 5350, + "6073-stored-collection-helpers.ts": 4150, + "6074-stored-collection-helpers-js.js": 3600, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From 9e9d467c69c9bffc2eaab20c3d4027fbe4b81554 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:08:12 -0500 Subject: [PATCH 53/67] Look up runtime features in sorted sets instead of comparison chains scanRuntimeFeatures matched libCall names with long `fn === "a" || fn === "b"` chains. Adding a case at the end edited the previous last line to add `||`, so changes that added cases to the same chain always conflicted. Each chain is now a sorted module-level set (typed as IrLibFn, so a misspelled name fails to compile), with prefix lists where a chain also matched by prefix. The intrinsic method chains become sets too. The features are unchanged: for every libCall in LIB_FN_SIGS and each string, array and typed-array method the scan tests, the old and new moduleRuntimeFeatures agree. --- packages/compiler/src/ir/runtime-features.ts | 321 ++++++++++--------- 1 file changed, 175 insertions(+), 146 deletions(-) diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 04443ee18..74bb08b46 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -123,6 +123,166 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet = new Set([ "util.debuglog", ]); +/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */ +const REGEX_LIB_FNS: ReadonlySet = new Set([ + "dyn.nativeRegexIs", + "fs.callbackGlob", + "fs.globSync", + "fs.globTypesSync", + "fsp.globError", + "fsp.globStrings", + "fsp.globTypes", + "path.matchesGlob", + "path.win32MatchesGlob", + "regexp.escape", + "rl.cursorPos", + "rl.keypressStdin", + "rl.keypressStream", + "stdin.onKeypress", + "util.stripVTControlCharacters", +]); + +/** TextDecoder entry points over the legacy encodings. */ +const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet = new Set([ + "text.decodeLegacy", + "text.decodeLegacyOptions", + "text.decodeStream", +]); + +/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */ +const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ + "http.clientSignal", + "rl.createSignal", + "rl.questionAsyncSignal", + "sp.finishedOptsSignal", + "sp.pipelineOptsSignal", + "stream.addAbortSignal", + "stream.addAbortSignalFn", + "stream.finishedOptsSignal", + "tp.schedulerWaitSignal", + "tp.setImmediateSignal", + "tp.setTimeoutSignal", +]); + +/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */ +const STREAM_LIB_FNS: ReadonlySet = new Set([ + "console.bind", + "crypto.hashStream", + "crypto.hashStreamPiped", + "fileHandle.createReadStream", + "fileHandle.createWriteStream", + "fs.createReadStream", + "fs.createWriteStream", + "sp.finished", + "sp.pipeline", + "sp.pipelineStages", + "stream.addAbortSignal", + "stream.addAbortSignalFn", + "stream.destroy", + "stream.destroyErr", + "stream.errored", + "stream.prop", + "stream.workerStdio", +]); + +/** libCall name prefixes that link the stream unit. */ +const STREAM_LIB_PREFIXES: readonly string[] = [ + "duplex.", + "fs.fsStream", + "passthrough.", + "readable.", + "sc.", + "sp.finishedOpts", + "sp.pipelineOpts", + "stream.finishedOpts", + "stream.pipelineStages", + "stream.set", + "transform.", + "writable.", + "zlib.stream", +]; + +/** The Readable operators and Readable.from over a compiled generator or a checked-dynamic iterator (scr_stream_ops.c), which await through the dyn-async unit (with READABLE_DYN_ASYNC_PREFIXES). */ +const READABLE_DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ + "readable.fromAsyncGen", + "readable.fromAsyncIter", + "readable.fromDone", + "readable.fromDyn", + "readable.fromIter", +]); + +/** libCall name prefixes of the Readable entry points that await through the dyn-async unit. */ +const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "readable.op"]; + +/** Entry points that link the dyn-invoke unit. */ +const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ + "dyn.arrayProtoCall", + "dyn.arrayPrototype", + "dyn.builtinMethod", + "dyn.defineProperty", + "dyn.defineProps", + "dyn.definePrototypeProps", + "dyn.functionApply", + "dyn.objCreateWithProperties", +]); + +/** Entry points that link the child-process unit (with CHILD_PROCESS_LIB_PREFIXES): the fork IPC channel on the process object. */ +const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([ + "process.connected", + "process.disconnect", + "process.forkTarget", + "process.onDisconnect", + "process.onMessage", + "process.send", + "process.sendCb", +]); + +/** libCall name prefixes that link the child-process unit. */ +const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."]; + +/** Entry points that link the symbol unit, besides the sym. and disposable. families. */ +const SYMBOL_LIB_FNS: ReadonlySet = new Set([ + "dyn.usingDispose", + "dyn.usingMethod", + "util.isDeepStrictEqual", +]); + +/** Entry points that link the querystring unit. */ +const QS_LIB_FNS: ReadonlySet = new Set(["qs.parse", "qs.stringify", "qs.unescape"]); + +/** Entry points that link the parseArgs unit. */ +const PARSE_ARGS_LIB_FNS: ReadonlySet = new Set([ + "util.isDeepStrictEqual", + "util.parseArgs", + "util.styleText", +]); + +/** String methods whose Unicode tables live in the regex unit's libunicode. */ +const CASE_MAPPING_STR_METHODS: ReadonlySet = new Set([ + "normalize", + "toLowerCase", + "toUpperCase", +]); + +/** Array methods that link the copying unit. */ +const COPYING_ARR_METHODS: ReadonlySet = new Set([ + "toReversed", + "toSpliced", + "with", + "withUndefined", +]); + +/** Typed-array methods that link the copying unit. */ +const COPYING_BYTES_METHODS: ReadonlySet = new Set([ + "join", + "toArray", + "toReversed", + "with", +]); + +const startsWithAny = (fn: string, prefixes: readonly string[]): boolean => + prefixes.some((prefix) => fn.startsWith(prefix)); + /** Inspect types and expressions together in one traversal. This is a fresh * snapshot: callers may mutate an IR module between compilation passes. */ /** The checked-dynamic undefined value (dynUndefinedExpr). */ @@ -239,126 +399,31 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { const fn = node.fn; - if ( - fn === "regexp.escape" || - fn === "dyn.nativeRegexIs" || - fn === "util.stripVTControlCharacters" || - fn === "path.matchesGlob" || - fn === "path.win32MatchesGlob" || - // fs.glob's walk compiles its patterns with the glob matcher. - fn === "fs.globSync" || - fn === "fs.globTypesSync" || - fn === "fs.callbackGlob" || - fn === "fsp.globStrings" || - fn === "fsp.globTypes" || - fn === "fsp.globError" - ) - features.regex = true; - if ( - fn === "text.decodeLegacy" || - fn === "text.decodeLegacyOptions" || - fn === "text.decodeStream" - ) - features.legacyTextDecoder = true; + if (REGEX_LIB_FNS.has(fn)) features.regex = true; + if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; - // Signal-taking readline, stream and timers/promises entry points - // register abort listeners. - if ( - fn === "rl.createSignal" || - fn === "rl.questionAsyncSignal" || - fn === "http.clientSignal" || - fn === "tp.setTimeoutSignal" || - fn === "tp.schedulerWaitSignal" || - fn === "tp.setImmediateSignal" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn === "sp.finishedOptsSignal" || - fn === "sp.pipelineOptsSignal" || - fn === "stream.finishedOptsSignal" - ) { - features.eventTarget = true; - } + if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; - // readline's terminal unit (scr_readline_tty.c) measures with the - // regex unit's stripVTControlCharacters; with a completer it prints - // completion errors through the inspect unit. - if ( - fn === "rl.cursorPos" || - fn === "rl.keypressStdin" || - fn === "rl.keypressStream" || - fn === "stdin.onKeypress" - ) - features.regex = true; + // With a completer, readline's terminal unit prints completion errors + // through the inspect unit. if (fn === "rl.terminal") { features.regex = true; if (!isDynUndefined(node.args[3]!)) features.inspect = true; } if (fn.startsWith("emitter.")) features.emitter = true; - if ( - fn.startsWith("readable.") || - fn.startsWith("writable.") || - fn.startsWith("duplex.") || - fn.startsWith("transform.") || - fn.startsWith("passthrough.") || - fn.startsWith("sc.") || - fn.startsWith("stream.set") || - fn === "stream.destroy" || - fn === "stream.destroyErr" || - fn === "stream.prop" || - fn === "stream.workerStdio" || - fn === "stream.errored" || - fn === "sp.finished" || - fn === "sp.pipeline" || - fn === "sp.pipelineStages" || - fn.startsWith("stream.pipelineStages") || - fn === "console.bind" || - fn === "crypto.hashStream" || - fn === "crypto.hashStreamPiped" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn.startsWith("sp.finishedOpts") || - fn.startsWith("sp.pipelineOpts") || - fn.startsWith("stream.finishedOpts") || - fn.startsWith("zlib.stream") || - fn === "fs.createReadStream" || - fn === "fs.createWriteStream" || - fn === "fileHandle.createReadStream" || - fn === "fileHandle.createWriteStream" || - fn.startsWith("fs.fsStream") - ) { + if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) { // the stream unit emits through the emitter unit, also when the // program constructs no stream (a pipeline of functions) features.stream = true; features.emitter = true; } if (fn.startsWith("zlib.")) features.zlib = true; - // The Readable operators and Readable.from over a compiled generator - // or a checked-dynamic iterator (scr_stream_ops.c) await through the - // dyn-async unit. - if ( - fn.startsWith("readable.op") || - fn.startsWith("readable.fromGen") || - fn === "readable.fromDyn" || - fn === "readable.fromIter" || - fn === "readable.fromAsyncIter" || - fn === "readable.fromAsyncGen" || - fn === "readable.fromDone" - ) + if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES)) features.dynAsync = true; if (fn.startsWith("dc.")) features.dc = true; if (fn.startsWith("assert.")) features.assert = true; - if ( - fn === "dyn.defineProps" || - fn === "dyn.definePrototypeProps" || - fn === "dyn.defineProperty" || - fn === "dyn.objCreateWithProperties" || - fn === "dyn.arrayProtoCall" || - fn === "dyn.arrayPrototype" || - fn === "dyn.functionApply" || - fn === "dyn.builtinMethod" - ) - features.dynInvoke = true; + if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true; if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; if ( fn.startsWith("insp.") || @@ -367,19 +432,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "util.styleText" ) features.inspect = true; - if ( - fn.startsWith("cp.") || - fn.startsWith("child.") || - fn.startsWith("writer.") || - fn.startsWith("spawnRes.") || - fn === "process.forkTarget" || - fn === "process.connected" || - fn === "process.send" || - fn === "process.sendCb" || - fn === "process.disconnect" || - fn === "process.onMessage" || - fn === "process.onDisconnect" - ) + if (CHILD_PROCESS_LIB_FNS.has(fn) || startsWithAny(fn, CHILD_PROCESS_LIB_PREFIXES)) features.childProcess = true; if (fn.startsWith("net.")) features.net = true; if (fn.startsWith("http.")) { @@ -392,18 +445,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.tls = true; } if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true; - if ( - fn.startsWith("sym.") || - fn === "util.isDeepStrictEqual" || - fn === "dyn.usingMethod" || - fn === "dyn.usingDispose" || - fn.startsWith("disposable.") - ) + if (fn.startsWith("sym.") || fn.startsWith("disposable.") || SYMBOL_LIB_FNS.has(fn)) features.symbol = true; if (fn === "process.hrtimeValue") features.bigint = true; if (fn.startsWith("bigint.")) features.bigint = true; if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true; - if (fn === "qs.parse" || fn === "qs.stringify" || fn === "qs.unescape") features.qs = true; + if (QS_LIB_FNS.has(fn)) features.qs = true; // scr_url_legacy.c lowercases and normalizes hostnames (the regex // unit's libunicode), parses and stringifies queries, and emits its // deprecation warnings through the dyn-async unit. @@ -412,8 +459,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.qs = true; features.dynAsync = true; } - if (fn === "util.parseArgs" || fn === "util.isDeepStrictEqual" || fn === "util.styleText") - features.parseArgs = true; + if (PARSE_ARGS_LIB_FNS.has(fn)) features.parseArgs = true; if (fn === "util.styleText") features.processEvents = true; // scr_util_callable.c: dyn wrappers over the descriptor, inspect, // regex (NODE_DEBUG matching) and terminal-color units. @@ -450,30 +496,13 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.regex = true; break; case "strIntrinsic": - if ( - node.method === "toLowerCase" || - node.method === "toUpperCase" || - node.method === "normalize" - ) - features.regex = true; + if (CASE_MAPPING_STR_METHODS.has(node.method)) features.regex = true; break; case "arrIntrinsic": - if ( - node.method === "toReversed" || - node.method === "toSpliced" || - node.method === "with" || - node.method === "withUndefined" - ) - features.copying = true; + if (COPYING_ARR_METHODS.has(node.method)) features.copying = true; break; case "bytesIntrinsic": - if ( - node.method === "toReversed" || - node.method === "with" || - node.method === "join" || - node.method === "toArray" - ) - features.copying = true; + if (COPYING_BYTES_METHODS.has(node.method)) features.copying = true; break; case "dynInvoke": features.dynInvoke = true; From 86e8a09d86dda51640ca8952fcb3c57696f70dd5 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:14:59 -0500 Subject: [PATCH 54/67] Sort ShapeHost's members and the lowerer's mode options by name Changes that add a backend host hook or a lowering option appended it at the end of ShapeHost, LowererMode and the Lowerer constructor's option assignments, so concurrent additions edited the same lines. ShapeHost's and LowererMode's members are now sorted by name with their comments, and the constructor assigns the plain mode options in name order, followed by the values derived from them in their previous order. Type-only and reordering changes: the emitted LLVM is unchanged. --- packages/compiler/src/backend/llvm/shapes.ts | 70 +++++++++---------- .../compiler/src/frontend/lowering/lowerer.ts | 65 ++++++++--------- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 951ed61c9..d49df20b2 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -61,64 +61,64 @@ import { LlvmUnsupportedError } from "./unsupported.js"; /** What the tables need from the emitter: the extern-declaration ledger * and the module-wide cycle/shape indexes. */ export interface ShapeHost { - declare(decl: string): void; /** The hierarchy root of a class (a program class or a runtime base), * or undefined for classes outside the class graph. */ classRootOf?(className: string): string | undefined; - /** Request the shared OOM abort helper (@sc_oom) — emitted once. */ - needOom(): void; + /** Byte offset from an object pointer back to ScrCycHdr.buf. */ + readonly cycleBufOffset: number; + declare(decl: string): void; + /** Inline the small-object allocator's fast paths (llvm/alloc.ts). */ + readonly inlineAlloc?: boolean; + /** Record and class fields that store a scalar union's tag and bits + * inline (inline-unions.ts): they own nothing. */ + readonly inlineUnions: InlineUnionFields; /** The immutable ScrMapOps descriptor (scr_map.h) of a Map or Set type: * a module constant holding the LLVM operands { key retain, key release, * key trace, value retain, value release, value trace, union key kinds }, * or `null` when every operand is null. */ mapOps(operands: readonly string[]): string; - /** The target C ABI's `size_t` / refcount integer type. */ - readonly sizeType: "i32" | "i64"; - /** Byte offset from an object pointer back to ScrCycHdr.buf. */ - readonly cycleBufOffset: number; - readonly tracedShapes: Set; - readonly tracedUnions: Set; + /** Request the shared OOM abort helper (@sc_oom) — emitted once. */ + needOom(): void; + /** The next collector statistics index for an emitted ScrCycType + * descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND + * in emission order. + * Absent: every descriptor gets 0, which keeps no statistics. */ + nextCycKind?(): number; /** Unions represented as a nullable arm pointer (nullable-unions.ts): * their RC and trace entry points are the arm's NULL-tolerant ones. */ readonly nullableUnions: NullableUnions; - /** Record and class fields that store a scalar union's tag and bits - * inline (inline-unions.ts): they own nothing. */ - readonly inlineUnions: InlineUnionFields; - /** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */ - unionNoneRef(): string; - readonly recordsById: Map; - readonly recordCloneShapes: ReadonlySet; - /** The program constructs lifetime regions: the ProgramLifetime classes - * allocate in regions and count references in their region chunk instead - * of being empty (llvm/regions.ts). */ - regions?: RegionHooks | null; - /** The alias tag of a runtime word that lifetime-region code reads and - * writes outside every field: the chunk count, the chunk owner, the class - * cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */ - regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string; /** Emit the live-object audit notes (`scr_obj_alloc_note` / * `scr_obj_free_note`) in emitted new/free helpers. Only runtimes built * with SCR_RC_AUDIT (the sanitized lane) count them; release and dev * runtime packs define them empty, so plain builds omit the calls. * Absent means emit. */ readonly objectAudit?: boolean; - /** Inline the small-object allocator's fast paths (llvm/alloc.ts). */ - readonly inlineAlloc?: boolean; - /** Worker executables: the inline allocator state is thread-local. */ - readonly threadLocalAlloc?: boolean; /** Inline RC fast-path helpers requested so far (":"); the * emitter defines each once through {@link emitInlineRcHelpers}. Null * keeps every retain/release a runtime call. */ readonly rcHelpers: Set | null; - /** Which fast paths are inlined (see {@link InlineRcMode}). */ - readonly rcMode?: InlineRcMode; /** Define the fast-path helpers `noinline` (see {@link inlineRcOption}). */ readonly rcHelpersNoinline?: boolean; - /** The next collector statistics index for an emitted ScrCycType - * descriptor (cycTypeDescriptor), counting from FIRST_EMITTED_CYC_KIND - * in emission order. - * Absent: every descriptor gets 0, which keeps no statistics. */ - nextCycKind?(): number; + /** Which fast paths are inlined (see {@link InlineRcMode}). */ + readonly rcMode?: InlineRcMode; + readonly recordCloneShapes: ReadonlySet; + readonly recordsById: Map; + /** The alias tag of a runtime word that lifetime-region code reads and + * writes outside every field: the chunk count, the chunk owner, the class + * cursors and scr_region_counting (", !tbaa !N", or "" in debug builds). */ + regionAliasTag?(word: "count" | "owner" | "cursor" | "counting"): string; + /** The program constructs lifetime regions: the ProgramLifetime classes + * allocate in regions and count references in their region chunk instead + * of being empty (llvm/regions.ts). */ + regions?: RegionHooks | null; + /** The target C ABI's `size_t` / refcount integer type. */ + readonly sizeType: "i32" | "i64"; + /** Worker executables: the inline allocator state is thread-local. */ + readonly threadLocalAlloc?: boolean; + readonly tracedShapes: Set; + readonly tracedUnions: Set; + /** The zeroed `%ScrUnion` an inline store reads for a NULL dummy. */ + unionNoneRef(): string; } /** What class emission asks of lifetime regions (LifetimeRegions in diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a77142cb0..2fb266be0 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -845,7 +845,25 @@ type RuntimeFenceTarget = /** The Lowerer's pass configuration (see lowerToIr). */ export interface LowererMode { + /** Symbols whose deferred diagnostics reachable emit already flushed — + * the remainder must not report them a second time. */ + alreadyFlushed?: ReadonlySet; + /** LowerOptions.externalTypes, threaded through every lowering pass. */ + externalTypes?: ReadonlyMap; + /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ + externalTypeSpecifiersByFile?: ReadonlyMap; + /** Program-validated ambient declaration symbols for each FFI name. + * Undefined in discovery's legacy call-local validation path. */ + ffiBindingSymbols?: ReadonlyMap>; + /** The build's outbound native FFI declarations. */ + ffiImports?: readonly IrFfiImport[]; + /** Statically resolved child_process.fork program roots, in stable target-id order. */ + forkTargets?: readonly ts.SourceFile[]; frontendServices?: FrontendServices | undefined; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; + /** LowerOptions.libraryCallbacks (see there). */ + libraryCallbacks?: boolean; projectCheckJs?: boolean; /** Names of bodies a prior reachability pass reached; null lowers everything. */ reachable?: ReadonlySet | null; @@ -853,35 +871,17 @@ export interface LowererMode { * always-reachable init bodies and module building, and report deferred * collection diagnostics nothing flushed. */ remainder?: boolean; - /** Symbols whose deferred diagnostics reachable emit already flushed — - * the remainder must not report them a second time. */ - alreadyFlushed?: ReadonlySet; - /** The build's target platform (LowerOptions.targetPlatform — lowerToIr - * passes it to every pass). Defaults to the host. */ - targetPlatform?: string; /** Node's startup refusal (preflight's resolution walk / named-import * link checks): %main opens with exactly this throw, before any * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; - /** LoadResult.inlineRequireCycleCalls. */ - inlineRequireCycleCalls?: ReadonlySet | undefined; - /** The build's outbound native FFI declarations. */ - ffiImports?: readonly IrFfiImport[]; - /** LowerOptions.libraryCallbacks (see there). */ - libraryCallbacks?: boolean; - /** Statically resolved child_process.fork program roots, in stable target-id order. */ - forkTargets?: readonly ts.SourceFile[]; - workerTargets?: readonly ts.SourceFile[]; - /** Program-validated ambient declaration symbols for each FFI name. - * Undefined in discovery's legacy call-local validation path. */ - ffiBindingSymbols?: ReadonlyMap>; - /** LowerOptions.externalTypes, threaded through every lowering pass. */ - externalTypes?: ReadonlyMap; - /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ - externalTypeSpecifiersByFile?: ReadonlyMap; + /** The build's target platform (LowerOptions.targetPlatform — lowerToIr + * passes it to every pass). Defaults to the host. */ + targetPlatform?: string; /** LowerOptions.tierMode, shared by every pass. */ tierMode?: TierMode; + workerTargets?: readonly ts.SourceFile[]; } function directExternalTypeSpecifiersByFile( @@ -2968,28 +2968,29 @@ export class Lowerer { readonly moduleOrder: ts.SourceFile[], mode: LowererMode = {}, ) { + // The mode's options, sorted by name; the values derived from them follow. + this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); + this.externalTypes = mode.externalTypes ?? new Map(); + this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; + this.ffiImports = mode.ffiImports ?? []; + this.forkTargets = mode.forkTargets ?? []; this.frontendServices = mode.frontendServices; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); + this.libraryCallbacks = mode.libraryCallbacks ?? false; + this.projectCheckJs = mode.projectCheckJs ?? false; this.reachable = mode.reachable ?? null; this.remainder = mode.remainder ?? false; - this.tierMode = mode.tierMode ?? "auto"; - this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); - this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; - this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); - this.projectCheckJs = mode.projectCheckJs ?? false; - this.ffiImports = mode.ffiImports ?? []; - this.libraryCallbacks = mode.libraryCallbacks ?? false; + this.targetPlatform = mode.targetPlatform ?? process.platform; + this.tierMode = mode.tierMode ?? "auto"; this.workerTargets = mode.workerTargets ?? []; this.workerTargets.forEach((sf, id) => { this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id); }); - this.forkTargets = mode.forkTargets ?? []; this.forkTargets.forEach((sf, id) => { this.forkTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id); }); this.ffiImportsByName = new Map(this.ffiImports.map((entry) => [entry.name, entry])); - this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null; - this.externalTypes = mode.externalTypes ?? new Map(); this.externalTypeSpecifiersByFile = mode.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(this.externalTypes); this.checker = program.getTypeChecker(); From b1395256d29f4c74143a5b6aa12c5e4922a71b6f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:17:31 -0500 Subject: [PATCH 55/67] Initialize the asyncContext runtime feature in declaration order The runtime feature scanner's result is serialized as JSON, and the self-hosted scanner lays its record out in the interface's field order. asyncContext was initialized before dynAsync, while the interface declares it after, so the self-hosted scanner's JSON put the key elsewhere than the TypeScript one. The initializer now follows the interface order. --- packages/compiler/src/ir/runtime-features.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index d50d173b8..e3a234260 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -218,12 +218,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run dc: false, assert: false, dynInvoke: mod.workers === true, - asyncContext: false, dynAsync: mod.workers === true || mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), + asyncContext: false, inspect: false, childProcess: false, net: false, From 60535fd09ceb77ce992e9cf78362b8d5afaf52f9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 00:21:03 -0500 Subject: [PATCH 56/67] Accept test and describe calls without a name, and settle skip and todo as Node does node:test changes, matching Node 24.22: - test, it, describe, suite, their skip/todo/only twins and t.test take Node's (name?, options?, fn?) forms: test(fn), test(options, fn), test(options) and test() compile, as does describe(fn) and describe(options, fn). The fallback declarations add the overloads. - A missing or empty name is the function's name, or "", as Node's Test constructor completes it. The name is the function expression's or declaration's own name, or the binding name of a const arrow or anonymous function; a name that is empty at run time takes the same default. - With both skip and todo, the test is skipped, and the directive message is the skip string, else the todo string. The skip and todo method twins override the option of the same name, so test.skip("x", { skip: "msg" }) reports a bare SKIP and it.skip("x", { todo: "msg" }) reports "msg". Before, the last option set the mode. The limitations page notes that a thrown object or array that is not an Error reports [object Object] in the failing section. The test-name-forms fixture compares the names, directives and counts with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../ambient/scriptc-node-fallback.d.ts | 53 +++-- .../src/frontend/lowering/lower-test.ts | 218 ++++++++++++++---- .../node-test/cases/test-name-forms.ts | 46 ++++ 4 files changed, 256 insertions(+), 62 deletions(-) create mode 100644 tests/fixtures/node-test/cases/test-name-forms.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 77d3d9a84..5e368cc03 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -4,3 +4,4 @@ - **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`. - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. +- **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 9655b7580..3e3f66a2a 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3405,8 +3405,10 @@ declare module "node:test" { readonly tags: readonly string[]; readonly assert: TestContextAssert; readonly mock: MockTracker; - test(name: string, fn?: TestFn): Promise; - test(name: string, options: TestOptions, fn?: TestFn): Promise; + test(name?: string, fn?: TestFn): Promise; + test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + test(options?: TestOptions, fn?: TestFn): Promise; + test(fn?: TestFn): Promise; skip(message?: string): void; todo(message?: string): void; diagnostic(message: string): void; @@ -3436,23 +3438,44 @@ declare module "node:test" { match(value: string, regExp: RegExp, message?: string): void; doesNotMatch(value: string, regExp: RegExp, message?: string): void; } - function test(name: string, fn?: TestFn): Promise; - function test(name: string, options: TestOptions, fn?: TestFn): Promise; + /* The name, options and function are each optional, as in Node: a test + * without a name takes the function's name, or "". */ + function test(name?: string, fn?: TestFn): Promise; + function test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function test(options?: TestOptions, fn?: TestFn): Promise; + function test(fn?: TestFn): Promise; namespace test { - function skip(name: string, fn?: TestFn): Promise; - function skip(name: string, options: TestOptions, fn?: TestFn): Promise; - function todo(name: string, fn?: TestFn): Promise; - function todo(name: string, options: TestOptions, fn?: TestFn): Promise; - function only(name: string, fn?: TestFn): Promise; - function only(name: string, options: TestOptions, fn?: TestFn): Promise; + function skip(name?: string, fn?: TestFn): Promise; + function skip(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function skip(options?: TestOptions, fn?: TestFn): Promise; + function skip(fn?: TestFn): Promise; + function todo(name?: string, fn?: TestFn): Promise; + function todo(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function todo(options?: TestOptions, fn?: TestFn): Promise; + function todo(fn?: TestFn): Promise; + function only(name?: string, fn?: TestFn): Promise; + function only(name?: string, options?: TestOptions, fn?: TestFn): Promise; + function only(options?: TestOptions, fn?: TestFn): Promise; + function only(fn?: TestFn): Promise; } const it: typeof test; - function describe(name: string, fn?: SuiteFn): void; - function describe(name: string, options: TestOptions, fn?: SuiteFn): void; + function describe(name?: string, fn?: SuiteFn): void; + function describe(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function describe(options?: TestOptions, fn?: SuiteFn): void; + function describe(fn?: SuiteFn): void; namespace describe { - function skip(name: string, fn?: SuiteFn): void; - function todo(name: string, fn?: SuiteFn): void; - function only(name: string, fn?: SuiteFn): void; + function skip(name?: string, fn?: SuiteFn): void; + function skip(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function skip(options?: TestOptions, fn?: SuiteFn): void; + function skip(fn?: SuiteFn): void; + function todo(name?: string, fn?: SuiteFn): void; + function todo(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function todo(options?: TestOptions, fn?: SuiteFn): void; + function todo(fn?: SuiteFn): void; + function only(name?: string, fn?: SuiteFn): void; + function only(name?: string, options?: TestOptions, fn?: SuiteFn): void; + function only(options?: TestOptions, fn?: SuiteFn): void; + function only(fn?: SuiteFn): void; } const suite: typeof describe; function before(fn: HookFn): void; diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index e36518559..bb525e02d 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -90,12 +90,14 @@ function atStringOf(expr: ts.CallExpression): string { return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`; } -/** One parsed { skip?, todo?, only? } options literal. `mode` is the - * runtime literal (0 run / 1 skip / 2 todo), `msg` the directive message - * expression ("" = none). */ +/** One parsed { skip?, todo?, only? } options literal. `skip` and `todo` + * are whether each is set (a string or true), and `skipMsg` and `todoMsg` + * the string messages (directiveOf settles them). */ interface TestOptions { - mode: number; - msg: IrExpr | null; + skip: boolean; + skipMsg: IrExpr | null; + todo: boolean; + todoMsg: IrExpr | null; only: boolean; /** The { plan } count, -1 without one. */ plan: number; @@ -109,8 +111,10 @@ interface TestOptions { } const noOptions = (): TestOptions => ({ - mode: 0, - msg: null, + skip: false, + skipMsg: null, + todo: false, + todoMsg: null, only: false, plan: -1, timeout: null, @@ -118,6 +122,23 @@ const noOptions = (): TestOptions => ({ pre: [], }); +/** The runtime mode (0 run / 1 skip / 2 todo) and directive message of a + * test or suite, as Node's Test constructor settles them: the skip and todo + * method twins override the options of the same name, skip wins over + * todo, and the message is the skip string, else the todo string. */ +function directiveOf(opts: TestOptions, methodMode: number, loc: SrcLoc): [IrExpr, IrExpr] { + if (methodMode === 1) { + opts.skip = true; + opts.skipMsg = null; + } + if (methodMode === 2) { + opts.todo = true; + opts.todoMsg = null; + } + const mode = opts.skip ? 1 : opts.todo ? 2 : 0; + return [numLit(mode, loc), opts.skipMsg ?? opts.todoMsg ?? strLit("", loc)]; +} + /** Flags 8 and 16: a { timeout } and a { concurrency } are given. */ const optionFlags = (opts: TestOptions): number => (opts.only ? 4 : 0) | (opts.timeout ? 8 : 0) | (opts.concurrency ? 16 : 0); @@ -168,15 +189,33 @@ function lowerTestOptions( // A shorthand entry's value is the binding of the same name. const init: ts.Expression = ts.isPropertyAssignment(prop) ? prop.initializer : key; if (name === "skip" || name === "todo") { - const mode = name === "skip" ? 1 : 2; const t = lowerer.typeOf(init); if (t.isStringLiteralType() || lowerer.mapTypeOf(t)?.kind === "string") { - out.mode = mode; - out.msg = optionValue(lowerer, out, lowerer.lowerExprExpecting(init, STRING), locOf(init)); - } else if (init.kind === ts.SyntaxKind.TrueKeyword) { - out.mode = mode; - } else if (init.kind === ts.SyntaxKind.FalseKeyword) { - // explicit false: run normally + const msg = optionValue( + lowerer, + out, + lowerer.lowerExprExpecting(init, STRING), + locOf(init), + ); + if (name === "skip") { + out.skip = true; + out.skipMsg = msg; + } else { + out.todo = true; + out.todoMsg = msg; + } + } else if ( + init.kind === ts.SyntaxKind.TrueKeyword || + init.kind === ts.SyntaxKind.FalseKeyword + ) { + const on = init.kind === ts.SyntaxKind.TrueKeyword; + if (name === "skip") { + out.skip = on; + out.skipMsg = null; + } else { + out.todo = on; + out.todoMsg = null; + } } else { lowerer.noLowering( `${what} with a non-literal ${name} value`, @@ -284,6 +323,110 @@ function lowerBodyArg( /** The shared registration lowering behind test/it and their skip/todo/ * only method twins — and, with `sub`, behind t.test. Argument shapes: * (name), (name, fn), (name, options, fn). */ +/** The `name` a function value has in JavaScript, when the compiler can + * tell: "" for an anonymous function expression, the declared or binding + * name otherwise; null for other expressions. */ +function staticFunctionName(lowerer: Lowerer, node: ts.Expression): string | null { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isArrowFunction(node)) return ""; + if (ts.isFunctionExpression(node)) return node.name?.text ?? ""; + if (!ts.isIdentifier(node)) return null; + const symbol = lowerer.resolveValueSymbol(node); + if (!symbol) return null; + for (const decl of lowerer.checker.declarationsOf(symbol)) { + if (ts.isFunctionDeclaration(decl)) return decl.name?.text ?? "default"; + if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && decl.initializer) { + let init = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) && init.name) return init.name.text; + if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) return decl.name.text; + } + } + return null; +} + +/** Node's (name?, options?, fn?) argument forms (`Test#createSubtest`): a + * leading function is the body, a leading object the options, otherwise + * the first argument is the name and a following object the options. */ +function splitTestArgs( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, +): { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } { + const args = expr.arguments; + const isFn = (a: ts.Expression): boolean => + lowerer.checker.getCallSignatures(lowerer.typeOf(a)).length > 0; + const isOptions = (a: ts.Expression): boolean => ts.isObjectLiteralExpression(a); + const unsupported = (): never => + lowerer.noLowering( + `${what} with these arguments`, + expr, + "the supported forms are (name?, options?, fn?) with a string name, an options object literal, and a function", + ); + if (args.length > 3 || args.some(ts.isSpreadElement)) unsupported(); + const out: { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } = {}; + let i = 0; + const next = args[i]; + if (next !== undefined && !isFn(next) && !isOptions(next)) { + out.nameNode = next; + i++; + } + const options = args[i]; + if (options !== undefined && isOptions(options)) { + out.optionsNode = options; + i++; + } + const fn = args[i]; + if (fn !== undefined && isFn(fn)) { + out.fnNode = fn; + i++; + } + if (i < args.length) unsupported(); + return out; +} + +/** The test's name, as Node's Test constructor completes it: a missing or + * empty name takes the function's name, or "". */ +function lowerTestName( + lowerer: Lowerer, + expr: ts.CallExpression, + what: string, + nameNode: ts.Expression | undefined, + fnNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr { + const fallback = (): string => { + const fnName = fnNode ? staticFunctionName(lowerer, fnNode) : ""; + if (fnName === null) { + lowerer.noLowering( + `${what} without a name, with a function whose name is not known at compile time`, + fnNode ?? expr, + "give the test a name, or pass a function expression or a declared function", + ); + } + return fnName || ""; + }; + if (!nameNode) return strLit(fallback(), loc); + const name = lowerer.lowerExprExpecting(nameNode, STRING); + if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name; + const slot = lowerer.declareHiddenLocal("%testName", STRING); + const value = varRef(slot.id, STRING, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: slot.id, init: name, loc }], + result: { + kind: "ternary", + cond: { kind: "strEq", negated: false, left: value, right: strLit("", loc), type: BOOL, loc }, + then: strLit(fallback(), loc), + else_: value, + type: STRING, + loc, + }, + type: STRING, + loc, + }; +} + function lowerRegistration( lowerer: Lowerer, expr: ts.CallExpression, @@ -293,21 +436,11 @@ function lowerRegistration( sub: IrExpr | null, // the lowered TestContext receiver (t.test) ): IrExpr { if (sub === null) requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 1 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name), (name, fn), and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const { nameNode, optionsNode, fnNode } = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, nameNode, fnNode, loc); let opts = noOptions(); - let fnNode: ts.Expression | undefined; - if (args.length === 2) fnNode = args[1]; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, false); - fnNode = args[2]; + if (optionsNode) { + opts = lowerTestOptions(lowerer, optionsNode, what, false); // The name runs before the option values. if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); @@ -315,11 +448,9 @@ function lowerRegistration( name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; if (methodMode === 3) opts.only = true; const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); const flagBits = optionFlags(opts); const planTimeout = [ numLit(opts.plan, loc), @@ -387,27 +518,21 @@ function lowerSuite( methodMode: number, ): IrExpr { requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; - if (args.length < 2 || args.length > 3) { - lowerer.noLowering( - `${what} with ${args.length} arguments`, - expr, - "the supported forms are (name, fn) and (name, options, fn)", - ); - } - let name = lowerer.lowerExprExpecting(args[0]!, STRING); + const split = splitTestArgs(lowerer, expr, what); + let name = lowerTestName(lowerer, expr, what, split.nameNode, split.fnNode, loc); let opts = noOptions(); - let fnNode = args[1]!; - if (args.length === 3) { - opts = lowerTestOptions(lowerer, args[1]!, what, true); - fnNode = args[2]!; + if (split.optionsNode) { + opts = lowerTestOptions(lowerer, split.optionsNode, what, true); if (opts.pre.length > 0 && name.kind !== "strLit") { const slot = lowerer.declareHiddenLocal("%testName", STRING); opts.pre.unshift({ kind: "varDecl", localId: slot.id, init: name, loc }); name = { kind: "varRef", localId: slot.id, type: STRING, loc }; } } - if (methodMode === 1 || methodMode === 2) opts.mode = methodMode; + const fnNode = split.fnNode; + if (!fnNode) { + lowerer.noLowering(`${what} without a body`, expr, "give the suite a body function"); + } const cb = lowerer.lowerExpr(fnNode); // The body may take the SuiteContext (flag 2, as test bodies take theirs). if ( @@ -429,8 +554,7 @@ function lowerSuite( ); } const at = strLit(atStringOf(expr), loc); - const mode = numLit(opts.mode, loc); - const msg = opts.msg ?? strLit("", loc); + const [mode, msg] = directiveOf(opts, methodMode, loc); // describe.only and { only: true } mark the suite (flag 4, as for tests). const flags = numLit( optionFlags(opts) | (methodMode === 3 ? 4 : 0) | (cb.type.params.length === 1 ? 2 : 0), diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts new file mode 100644 index 000000000..915d68a79 --- /dev/null +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -0,0 +1,46 @@ +// The (name?, options?, fn?) forms of test, it, describe and t.test, and +// Node's default names: a missing or empty name takes the function's name, +// or "". With both skip and todo, skip wins, in either order and +// against the todo method twins. +import { describe, it, test } from "node:test"; + +function declaredTest(): void {} +const boundArrow = (): void => {}; +const emptyName = process.argv.length > 99 ? "never" : ""; + +test(function namedExpression() {}); +test(() => {}); +test(declaredTest); +test(boundArrow); +test("", boundArrow); +test(emptyName, declaredTest); +test({ skip: "options first" }, () => {}); +test({ todo: true }); +test(); +test("name and options", { skip: true }); + +describe(function namedSuite() { + it(() => {}); + it({ todo: "later" }, function insideTodo() {}); +}); +describe({ skip: "whole suite" }, () => { + it("not run", () => {}); +}); + +test("subtests", async (t) => { + await t.test(function namedSub() {}); + await t.test(() => {}); + await t.test({ skip: true }, () => {}); +}); + +test("skip then todo", { skip: "skip message", todo: "todo message" }, () => {}); +test("todo then skip", { todo: "todo message", skip: true }, () => {}); +test.todo("todo twin with skip", { skip: true }, () => {}); +it.skip("skip twin with todo", { todo: true }, () => {}); +// The twins override the option of the same name, and the message is the +// skip string, else the todo string. +test.skip("skip twin with a skip message", { skip: "option message" }, () => {}); +it.todo("todo twin with a todo message", { todo: "option message" }, () => {}); +it.skip("skip twin with a todo message", { todo: "todo message" }, () => {}); +test.todo("todo twin with a skip message", { skip: "skip message" }, () => {}); +test("skip false with a todo message", { skip: false, todo: "todo message" }, () => {}); From 496cf577c73793a0ea9815e051067ae463d4401d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 01:10:39 -0500 Subject: [PATCH 57/67] Print iterators and iterator helper objects as Node does console.log and util.inspect of a generator object, an iterator helper, or an array or string iterator were refused, and printing a Map or Set iterator threw "util.inspect of a dynamic Array Iterator is not supported yet". Compiled generators now carry the tag their object prints with: "Generator" or "AsyncGenerator" by default, "Iterator Helper" for the helpers, "Array Iterator" and "String Iterator" for the iterators the compiler builds as generators. The spawn wrapper sets it through the new scr_gen_set_tag; an adapter over another generator copies that generator's tag (scr_gen_copy_tag), and generators that stand in for an object of another class, such as a Set iterator adapted into an iterator-typed slot, keep refusing to print. They print as Node's Object [] {} at any depth, and Object.prototype.toString answers [object ]. The runtime's native iterators print as Node does: Object [Array Iterator] {} and Object [String Iterator] {}, and for Map and Set iterators the entries the next steps would produce, without advancing, under [Set Iterator], [Set Entries], [Map Iterator] or [Map Entries], as [Object [Set Iterator]] past the depth limit and { } when none are left. Object.prototype.toString answers [object Set Iterator] and the other tags. --- .../builtin-apis/iterator-helpers.mdx | 2 +- packages/compiler/src/backend/llvm/emitter.ts | 9 ++ .../lowering/containers/iterator-helpers.ts | 1 + .../frontend/lowering/iterator-adapters.ts | 22 +++-- .../src/frontend/lowering/lower-calls.ts | 11 ++- .../src/frontend/lowering/lower-inspect.ts | 12 +++ .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 9 ++ .../tests/corpus/6075-print-iterators.ts.json | 6 ++ .../corpus/6076-print-iterators-js.js.json | 6 ++ packages/runtime/src/scr_async.c | 9 ++ packages/runtime/src/scr_dyn.h | 6 ++ packages/runtime/src/scr_generator.h | 10 ++ packages/runtime/src/scr_inspect.c | 98 +++++++++++++++++++ packages/runtime/src/scr_json.c | 31 ++++++ tests/corpus/6075-print-iterators.ts | 19 ++++ tests/corpus/6076-print-iterators-js.js | 17 ++++ tests/harness/corpus-costs.json | 4 + 18 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json create mode 100644 tests/corpus/6075-print-iterators.ts create mode 100644 tests/corpus/6076-print-iterators-js.js diff --git a/docs/content/limitations/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index cb7cab1f4..b4c4d8200 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -1 +1 @@ -- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported. +- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, typed array, string, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `.return()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. In JavaScript, untyped array, string, Map and Set iterators answer the same methods. `console.log`, `util.inspect` and `Object.prototype.toString` print iterator objects as Node does: `Object [Iterator Helper] {}`, `Object [Generator] {}`, `Object [Array Iterator] {}`, and Map and Set iterators with the entries they have left. `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and `String()` and template literals of typed iterator objects are unsupported. diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 872153f40..48012704b 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4644,6 +4644,9 @@ export class LlEmitter { // An iterator helper's return() before its first next() closes the // source generator in its first argument slot. const closesSource = fn.generator.closesSource === true; + if (fn.generator.tag !== undefined) this.declare(`declare void @scr_gen_set_tag(ptr, ptr)`); + if (fn.generator.inheritsTag === true) + this.declare(`declare void @scr_gen_copy_tag(ptr, ptr)`); if (closesSource) { const field = lifted ? 1 : 0; this.declare(`declare void @scr_gen_close(ptr)`); @@ -4676,6 +4679,12 @@ export class LlEmitter { settleAsync === null ? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, + ...(fn.generator.tag !== undefined + ? [` call void @scr_gen_set_tag(ptr %gg, ptr ${this.cstr(fn.generator.tag)})`] + : []), + ...(fn.generator.inheritsTag === true + ? [` call void @scr_gen_copy_tag(ptr %gg, ptr %a${lifted ? 1 : 0})`] + : []), ...(closesSource ? [ ` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`, diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 229999627..79b91af64 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -2259,6 +2259,7 @@ function buildHelperGenFn( yieldT: outT.yieldT, nextT: outT.nextT, resultType, + tag: "Iterator Helper", // A native Map or Set iterator has no return() to call. ...(source.kind === "generator" ? { closesSource: true as const } : {}), }, diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index f086417de..898c46f20 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -258,7 +258,7 @@ function dynIteratorAdapter( returnType: expected.retT, locals, body, - generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType }, + generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType, tag: "!" }, loc, }); } @@ -374,7 +374,12 @@ export function lowerCollectionIteratorValue( loc, }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { + yieldT: type.yieldT, + nextT: UNDEFINED_T, + resultType, + tag: "!", + }, loc, }); } @@ -484,7 +489,7 @@ export function lowerBytesIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "Array Iterator" }, loc, }); } @@ -590,7 +595,7 @@ export function lowerStringIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "String Iterator" }, loc, }); } @@ -751,7 +756,12 @@ function generatorRetypeAdapter( loc, }, ], - generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType: toResult }, + generator: { + yieldT: expected.yieldT, + nextT: expected.nextT, + resultType: toResult, + inheritsTag: true, + }, loc, }); } @@ -1022,7 +1032,7 @@ export function lowerArrayIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "Array Iterator" }, loc, }); } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 58cbb9543..b3cd7e39b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -2800,7 +2800,8 @@ function lowerObjectPrototypeCall( const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); - if (tag === null && receiver.type.kind !== "dyn") { + // A generator object answers its tag through its checked capsule. + if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") { lowerer.noLowering( "Object.prototype.toString.call over a " + lowerer.fmt(receiver.type) + " receiver", receiverNode, @@ -2820,7 +2821,13 @@ function lowerObjectPrototypeCall( } const result: IrExpr = tag === null - ? { kind: "libCall", fn: "dyn.objectTag", args: [ref], type: STRING, loc } + ? { + kind: "libCall", + fn: "dyn.objectTag", + args: [ref.type.kind === "generator" ? lowerer.coerceToExpected(ref, DYN) : ref], + type: STRING, + loc, + } : { kind: "strLit", value: "[object " + tag + "]", type: STRING, loc }; return { kind: "seqExpr", stmts, result, type: STRING, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1d67cef92..2551031eb 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -253,6 +253,10 @@ function inspectSupport( visiting.delete(t.className); return null; } + // Generator objects print through their checked capsule: Node's + // `Object [Generator] {}`, or the tag of the iterator they implement. + case "generator": + return null; default: return `'${lowerer.fmt(t)}' values have no inspect lowering`; } @@ -343,6 +347,14 @@ export function inspectExpr( return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc }; case "dyn": return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc }; + case "generator": + return { + kind: "libCall", + fn: "insp.dyn", + args: [lowerer.coerceToExpected(value, DYN), recurse, depth], + type: STRING, + loc, + }; case "object": if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) { return { diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index ab4b05c8b..117bba24a 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -143,6 +143,7 @@ const FUNCTION_FIELDS: Record>, Role> = { "captures[].localId": "other", "classCaptures[].name": "other", "classCaptures[].localId": "other", + "generator.tag": "other", }; const MODULE_FIELDS: Record>, Role> = { diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 0aa510b8d..751c6c021 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1712,6 +1712,15 @@ export interface IrFunction { * iterator helper's source): `.return()` before the first `.next()` * closes it. */ closesSource?: true; + /** The @@toStringTag the object prints with (util.inspect, + * Object.prototype.toString) when it is not a generator object: + * "Iterator Helper", "Array Iterator", "String Iterator". A tag that + * starts with '!' marks a generator standing in for an object of + * another class, which those refuse to print. */ + tag?: string; + /** The first parameter is a generator this one adapts: it prints with + * that generator's tag. */ + inheritsTag?: true; }; /** Source function name for native error stack frames. */ sourceName?: string; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json new file mode 100644 index 000000000..04adc830d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6075-print-iterators.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6075-print-iterators.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json new file mode 100644 index 000000000..255e32494 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6076-print-iterators-js.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6076-print-iterators-js.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index b1ebfc6e9..124304970 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4986,6 +4986,7 @@ struct ScrGen { void (*drop_args)(void *); /* scr_gen_set_unstarted_return's hook; NULL for most generators. */ void (*unstarted_return)(void *); + const char *tag; /* scr_gen_set_tag's; NULL is the default */ bool is_async; void (*settle_async)(ScrGen *, ScrPromise *); ScrAsyncGenRequest *requests_head; @@ -5547,6 +5548,14 @@ void scr_gen_resume(ScrGen *g) { void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; } +void scr_gen_set_tag(ScrGen *g, const char *tag) { g->tag = tag; } + +void scr_gen_copy_tag(ScrGen *g, const ScrGen *from) { g->tag = from->tag; } + +const char *scr_gen_tag(const ScrGen *g) { + return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator"; +} + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index e76563ab7..4938ee01d 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -706,6 +706,12 @@ bool scr_dyn_iterator_step_done(const ScrDyn *iterator); * next live entry index of its collection, or -1 once the collection is * exhausted (the iterator then ends its iteration). */ double scr_dyn_collection_iterator_index(const ScrDyn *iterator); +/* For util.inspect: a native iterator's kind (0 array or typed array, + * 1 string, 2 Map, 3 Set, 4 a handle's), its selection (0 values, 1 keys, + * 2 entries), its collection (NULL once exhausted, and for kinds other + * than 2 and 3) and the index its next step reads from. */ +void scr_dyn_native_iterator_state(const ScrDyn *iterator, unsigned *kind, unsigned *selection, + ScrMap **collection, double *index); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index a92cd3222..76d152f8a 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -63,6 +63,16 @@ ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, /* Runs when return() reaches the generator before its first resume, with * the packed arguments still alive: an iterator helper closes its source. */ void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)); +/* The @@toStringTag that util.inspect and Object.prototype.toString print + * for the generator object: "Generator" (or "AsyncGenerator") unless set, + * as the compiler does for iterator helpers ("Iterator Helper") and for + * the array and string iterators it builds as generators. A tag that + * starts with '!' marks a generator standing in for an object of another + * class, which these refuse to print. */ +void scr_gen_set_tag(ScrGen *g, const char *tag); +/* An adapter generator prints with the tag of the generator it adapts. */ +void scr_gen_copy_tag(ScrGen *g, const ScrGen *from); +const char *scr_gen_tag(const ScrGen *g); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 4aa703711..019f36536 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1058,6 +1058,99 @@ static ScrStr *scr_insp_weak_collection(const char *label, double recurse, doubl return ib_take(&out); } +/* A native iterator: the array and string iterators print Node's empty + * `Object [Array Iterator] {}`; the Map and Set iterators print the + * entries their next steps would produce, under Node's labels + * (`[Set Iterator] { 1, 2 }`, `[Map Entries] { [ 'a', 1 ] }`), without + * advancing. NULL for a handle's iterator (kind 4), which keeps the + * unsupported answer. */ +static ScrStr *scr_insp_native_iterator(ScrDyn *d, double recurse, double depth) { + unsigned kind, selection; + ScrMap *map; + double index; + scr_dyn_native_iterator_state(d, &kind, &selection, &map, &index); + if (kind == 0) return scr_str_new("Object [Array Iterator] {}", 26); + if (kind == 1) return scr_str_new("Object [String Iterator] {}", 27); + if (kind != 2 && kind != 3) return NULL; + bool pairs = selection == 2; + const char *label = kind == 3 ? (pairs ? "[Set Entries]" : "[Set Iterator]") + : (pairs ? "[Map Entries]" : "[Map Iterator]"); + if (recurse > depth) { + InspBuf out = {0}; + ib_cstr(&out, "[Object "); + ib_cstr(&out, label); + ib_char(&out, ']'); + return ib_take(&out); + } + size_t remaining = 0; + if (map) { + for (double i = scr_map_iter_skip(map, index); i < scr_map_iter_count(map); i++) + if (scr_map_iter_live(map, i)) remaining++; + } + if (remaining == 0) { + InspBuf out = {0}; + ib_cstr(&out, label); + ib_cstr(&out, " { }"); + return ib_take(&out); + } + ScrStr *base = scr_str_new(label, strlen(label)); + scr_insp_begin(recurse + 1); + scr_map_iter_enter(map); + size_t shown = 0; + for (double i = scr_map_iter_skip(map, index); i < scr_map_iter_count(map) && shown < 100; i++) { + if (!scr_map_iter_live(map, i)) continue; + shown++; + ScrDyn *item; + if (pairs) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_map_dyn_key(map, i)); + scr_dyn_arr_push(item, kind == 3 ? scr_map_dyn_key(map, i) : scr_map_dyn_value(map, i)); + } else { + item = kind == 2 && selection == 0 ? scr_map_dyn_value(map, i) : scr_map_dyn_key(map, i); + } + ScrStr *text = scr_insp_dyn(item, recurse + 1, depth); + scr_dyn_release(item); + scr_insp_entry(text, false); + scr_str_release(text); + } + scr_map_iter_exit(map); + if (remaining > shown) { + ScrStr *more = scr_insp_more_items((double)(remaining - shown)); + scr_insp_entry(more, false); + scr_str_release(more); + } + ScrStr *b0 = scr_str_new("{", 1); + ScrStr *b1 = scr_str_new("}", 1); + ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, false, false); + scr_str_release(base); + scr_str_release(b0); + scr_str_release(b1); + return out; +} + +/* A compiled generator object: Node's empty `Object [] {}`, with the + * tag the compiler set (scr_gen_set_tag). A generator standing in for an + * object of another class (a tag starting with '!') throws the + * "not supported yet" Error instead. */ +static bool scr_insp_is_generator(const ScrDyn *d) { + const char *key = d->v.typed_ref.type_key; + return !strncmp(key, "generator<", 10) || !strncmp(key, "async-generator<", 16); +} + +static ScrStr *scr_insp_generator(const ScrDyn *d) { + const char *tag = scr_gen_tag(d->v.typed_ref.ptr); + if (tag[0] == '!') { + static const char message[] = "util.inspect of this iterator object is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return scr_str_new("", 0); + } + InspBuf out = {0}; + ib_cstr(&out, "Object ["); + ib_cstr(&out, tag); + ib_cstr(&out, "] {}"); + return ib_take(&out); +} + static ScrStr *scr_insp_dyn_collection(ScrDyn *d, double recurse, double depth, bool is_map) { ScrMap *map = d->v.handle.ptr; const char *label = is_map ? "Map" : "Set"; @@ -1309,6 +1402,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { return scr_insp_weak_collection(d->v.handle.tag == SCR_DYNH_WEAK_MAP ? "WeakMap" : "WeakSet", recurse, depth); if (d->v.handle.tag == SCR_DYNH_DATE) return scr_insp_date(scr_dyn_native_date_value(d)); + if (d->v.handle.tag == SCR_DYNH_ITERATOR) { + ScrStr *iterator = scr_insp_native_iterator(d, recurse, depth); + if (iterator) return iterator; + } const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(d); if (ops->inspect_view) { /* Node's custom inspectors answer the bare class name beyond the @@ -1349,6 +1446,7 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { } case SCR_DYN_TYPED_REF: { + if (scr_insp_is_generator(d)) return scr_insp_generator(d); const char *cls = NULL; ScrDyn *view = scr_event_ref_hooks ? scr_event_ref_hooks->inspect_view(d, &cls) : NULL; if (view) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index d18b3d910..206c01839 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -4673,6 +4673,16 @@ bool scr_dyn_iterator_step_done(const ScrDyn *value) { return ((ScrNativeIterator *)value->v.handle.ptr)->step_done; } +void scr_dyn_native_iterator_state(const ScrDyn *value, unsigned *kind, unsigned *selection, + ScrMap **collection, double *index) { + const ScrNativeIterator *iterator = value->v.handle.ptr; + *kind = iterator->kind; + *selection = iterator->selection; + *collection = (iterator->kind == 2 || iterator->kind == 3) && iterator->source + ? iterator->source->v.handle.ptr : NULL; + *index = iterator->index; +} + double scr_dyn_collection_iterator_index(const ScrDyn *value) { ScrNativeIterator *iterator = value->v.handle.ptr; ScrDyn *source = iterator->source; @@ -6217,6 +6227,20 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { } if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); + if (scr_dyn_generator(d)) { + const char *gen_tag = scr_gen_tag(d->v.typed_ref.ptr); + if (gen_tag[0] == '!') { + static const char message[] = "Object.prototype.toString of this iterator is not supported yet"; + scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); + return NULL; + } + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "[object "); + scr_jb_puts(&b, gen_tag); + scr_jb_putc(&b, ']'); + return scr_jb_finish(&b); + } ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); ScrStr *out = scr_dyn_object_tag(materialized); scr_dyn_release(materialized); @@ -6249,6 +6273,13 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { scr_jb_putc(&b, ']'); return scr_jb_finish(&b); } + if (d->v.handle.tag == SCR_DYNH_ITERATOR && ((const ScrNativeIterator *)d->v.handle.ptr)->kind < 4) { + static const char *const tags[] = { + "[object Array Iterator]", "[object String Iterator]", "[object Map Iterator]", "[object Set Iterator]", + }; + const char *it_tag = tags[((const ScrNativeIterator *)d->v.handle.ptr)->kind]; + return scr_str_new(it_tag, strlen(it_tag)); + } if (scr_dyn_handle_ops_of(d)->to_string_tag) { ScrJsonBuf b; scr_jb_init(&b); diff --git a/tests/corpus/6075-print-iterators.ts b/tests/corpus/6075-print-iterators.ts new file mode 100644 index 000000000..8b4833264 --- /dev/null +++ b/tests/corpus/6075-print-iterators.ts @@ -0,0 +1,19 @@ +// util.inspect and Object.prototype.toString of iterator objects: generators, +// array, typed array and string iterators and iterator helpers print as +// Object [] {}, and Map and Set iterators print the entries they have +// left. +import { inspect } from "node:util"; +function* g(): Generator { yield 1; } +const s = new Set([1, 2, 3]); +const m = new Map([["a", 1], ["b", 2]]); +const si = s.values(); si.next(); +console.log(g(), [1, 2].values(), "ab"[Symbol.iterator](), new Uint8Array([1]).values()); +console.log(s.values(), s.entries(), m.keys(), m.values(), m.entries(), si); +console.log([1].values().map((x) => x), Iterator.from([1]), g().filter(() => true)); +console.log(inspect({ a: { b: { c: s.values(), d: g() } } })); +console.log(Object.prototype.toString.call(g()), Object.prototype.toString.call([1].values().map((x) => x)), Object.prototype.toString.call(m.keys())); +const done = s.values(); Array.from(done); +console.log(done, new Map().entries(), g()); +const keyed = new Map<{ x: number }, number[]>([[{ x: 1 }, [1, 2]]]); +console.log(keyed.entries(), keyed.keys()); +console.log(new Set(Array.from({ length: 120 }, (_, i) => i)).values()); diff --git a/tests/corpus/6076-print-iterators-js.js b/tests/corpus/6076-print-iterators-js.js new file mode 100644 index 000000000..ad4022b6b --- /dev/null +++ b/tests/corpus/6076-print-iterators-js.js @@ -0,0 +1,17 @@ +// The JavaScript form: util.inspect and Object.prototype.toString of +// iterator objects. +import { inspect } from "node:util"; +function* g() { yield 1; } +const s = new Set([1, 2, 3]); +const m = new Map ([["a", 1], ["b", 2]]); +const si = s.values(); si.next(); +console.log(g(), [1, 2].values(), "ab"[Symbol.iterator](), new Uint8Array([1]).values()); +console.log(s.values(), s.entries(), m.keys(), m.values(), m.entries(), si); +console.log([1].values().map((x) => x), Iterator.from([1]), g().filter(() => true)); +console.log(inspect({ a: { b: { c: s.values(), d: g() } } })); +console.log(Object.prototype.toString.call(g()), Object.prototype.toString.call([1].values().map((x) => x)), Object.prototype.toString.call(m.keys())); +const done = s.values(); Array.from(done); +console.log(done, new Map ().entries(), g()); +const keyed = new Map ([[{ x: 1 }, [1, 2]]]); +console.log(keyed.entries(), keyed.keys()); +console.log(new Set(Array.from({ length: 120 }, (_, i) => i)).values()); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index c37b780ea..10d9c3aca 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2242,6 +2242,8 @@ "6072-iterator-sources-js.js": 5350, "6073-stored-collection-helpers.ts": 4000, "6074-stored-collection-helpers-js.js": 4100, + "6075-print-iterators.ts": 3800, + "6076-print-iterators-js.js": 3500, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -5320,6 +5322,8 @@ "6072-iterator-sources-js.js": 5350, "6073-stored-collection-helpers.ts": 4150, "6074-stored-collection-helpers-js.js": 3600, + "6075-print-iterators.ts": 3450, + "6076-print-iterators-js.js": 3300, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, From e63d0e51c906b1d491d72dc8677dace5ed4c3a95 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 01:14:20 -0500 Subject: [PATCH 58/67] Wait before returning parked workers' memory, and discard one waiting arena to claim one When the last running worker parks, the trim helper now waits SCR_SA_DISCARD_DELAY_MS (100 by default) before it returns the parked workers' pages, and stops if a worker starts meanwhile. A program that exits within the delay, such as a one-shot type check whose checkers finish just before it reports and exits, no longer returns pages during its last milliseconds. A thread that finds every arena claimed discards one waiting arena and claims it, instead of running every waiting discard. --- packages/runtime/src/scr_alloc.c | 42 ++++++++++++++++++---- packages/runtime/src/scr_alloc.test.c | 52 ++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 2212a1a36..246d27f0a 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -638,6 +638,7 @@ static _Thread_local ScrSaKeep *scr_sa_keep; /* while the worker's memory is kep static void scr_sa_keep_begin(unsigned i); static void scr_sa_keep_drained(ScrSaKeep *k, ScrSaBlock *b, unsigned c); static bool scr_sa_drain(void); +static bool scr_sa_drain_some(bool one); #endif /* Moves every block other threads freed into this arena's free lists. */ @@ -672,8 +673,8 @@ static bool scr_sa_reserve(void) { if (claimed == UINT32_MAX) { #ifdef SCR_SA_TRIM /* Arenas held by discards that wait (see scr_sa_thread_park): this - * thread discards them and claims one. */ - if (scr_sa_drain()) { + * thread discards one of them and claims it. */ + if (scr_sa_drain_some(true)) { scr_sa_refused = false; claimed = atomic_load_explicit(&scr_sa_claimed, memory_order_relaxed); continue; @@ -934,6 +935,12 @@ static void scr_sa_trim(void) { * interrupts every running thread of the process to flush its TLB, and * glibc's trim locks the arenas other threads allocate from, so discards * wait for the last worker of a group, which hands them to one helper; + * - the helper waits SCR_SA_DISCARD_DELAY_MS (100 by default) and stops if + * a worker started meanwhile: a program that starts its next workers or + * exits within that time does not return pages it would fault back in or + * never touch again, and the last worker of the next group hands the + * discards to a new helper; a thread that finds every arena claimed + * discards one waiting arena and claims it; * - the helper then returns the pages * it carved (MADV_DONTNEED; the start of the first page, which it may * share with older blocks, is cleared), frees the recorded system blocks, @@ -1276,11 +1283,13 @@ static void scr_sa_defer_discard(unsigned i, ScrSaKeep *k) { pthread_mutex_unlock(&scr_sa_pending_lock); } -/* Runs the waiting discards on the calling thread, which ends with no arena; - * returns whether there were any. */ -static bool scr_sa_drain(void) { +/* Runs the waiting discards on the calling thread, which ends with no arena: + * all of them, or only the first one when `one` is set; returns whether + * there were any. */ +static bool scr_sa_drain_some(bool one) { bool any = false; for (;;) { + if (one && any) return true; pthread_mutex_lock(&scr_sa_pending_lock); uint32_t pending = scr_sa_pending; unsigned i = pending ? (unsigned)__builtin_ctz(pending) : 0; @@ -1298,9 +1307,30 @@ static bool scr_sa_drain(void) { } } +static bool scr_sa_drain(void) { return scr_sa_drain_some(false); } + +/* SCR_SA_DISCARD_DELAY_MS: how long the trim helper waits after the last + * worker parks before it returns their memory (default 100). */ +static long scr_sa_discard_delay_ms(void) { + const char *delay = getenv("SCR_SA_DISCARD_DELAY_MS"); + if (!delay) return 100; + long ms = strtol(delay, NULL, 10); + return ms < 0 ? 0 : ms; +} + +/* Waits out the delay first, and stops if a worker starts meanwhile: the + * discards then wait for the last worker of that group. A program that exits + * within the delay does not spend its last milliseconds returning pages, and + * one that starts its next workers within it does not return pages while + * they start. */ static void *scr_sa_drain_main(void *arg) { (void)arg; - scr_sa_drain(); + for (long left = scr_sa_discard_delay_ms(); left > 0; left -= 5) { + struct timespec pause = {0, (left < 5 ? left : 5) * 1000000L}; + nanosleep(&pause, NULL); + if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) != 0) return NULL; + } + if (atomic_load_explicit(&scr_sa_workers_live, memory_order_acquire) == 0) scr_sa_drain(); return NULL; } diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index f0efdc935..aa6b0b60b 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -627,7 +627,7 @@ static void *small_worker(void *arg) { /* Discards wait while another worker runs, and the last worker to park * starts them. When every arena is held by a discard that waits, a worker - * that needs one runs them itself. */ + * that needs one discards one of them and claims it. */ static void check_deferred_discard(void) { size_t page = (size_t)sysconf(_SC_PAGESIZE); atomic_store(&discard_step, 0); @@ -656,6 +656,55 @@ static void check_deferred_discard(void) { discard_in_old_arena = true; } +/* A plain thread that holds an arena until discard_step reaches 3. */ +static void *holding_thread(void *arg) { + (void)arg; + void *p = scr_mem_alloc(DISCARD_SIZE); + assert(p && owned(p)); + atomic_store(&discard_step, 1); + wait_step(3); + scr_mem_free(p); + scr_sa_thread_park(); + return NULL; +} + +/* The helper waits SCR_SA_DISCARD_DELAY_MS before it returns the pages, and + * stops if a worker starts meanwhile; the last worker of that group starts + * the discards again. A plain thread holds the arena whose slice the earlier + * checks filled, so the worker carves a slice of its own. */ +static void check_discard_delay(void) { + size_t page = (size_t)sysconf(_SC_PAGESIZE); + setenv("SCR_SA_DISCARD_DELAY_MS", "300", 1); + atomic_store(&discard_step, 0); + pthread_t plain; + assert(pthread_create(&plain, NULL, holding_thread, NULL) == 0); + wait_step(1); + discard_shares = false; + discard_in_old_arena = false; + pthread_t t; + assert(pthread_create(&t, NULL, discard_worker, NULL) == 0); + assert(pthread_join(t, NULL) == 0); + atomic_store(&discard_step, 0); + pthread_t holder; + assert(pthread_create(&holder, NULL, holding_worker, NULL) == 0); + wait_step(1); + /* Past the delay: the helper stopped when the holder started. */ + struct timespec pause = {0, 600000000L}; + nanosleep(&pause, NULL); + uintptr_t first = (discard_lo + page - 1) & ~(uintptr_t)(page - 1); + size_t total = (discard_hi - first) / page; + assert(resident_pages(first, discard_hi) > total / 2); + for (size_t i = 0; i < DISCARD_BLOCKS; i += 4099) verify(discard_blocks[i], block_size, (unsigned char)i); + setenv("SCR_SA_DISCARD_DELAY_MS", "0", 1); + atomic_store(&discard_step, 3); + assert(pthread_join(holder, NULL) == 0); + assert(pthread_join(plain, NULL) == 0); + scr_sa_wait_trims(); + unsetenv("SCR_SA_DISCARD_DELAY_MS"); + assert(resident_pages(first, discard_hi) < total / 2); + discard_in_old_arena = true; +} + static void check_discard(void) { discard_blocks = malloc(DISCARD_BLOCKS * sizeof *discard_blocks); assert(discard_blocks); @@ -750,6 +799,7 @@ static void check_discard(void) { free(trim_blocks); check_deferred_discard(); + check_discard_delay(); for (unsigned i = 0; i < DISCARD_OLD; i++) scr_mem_free(old_blocks[i]); free(discard_blocks); } From 192ea32474e299bff72057b30a7306489ab08f88 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 02:40:22 -0500 Subject: [PATCH 59/67] Read generator tags through a pointer the generator runtime installs scr_json.c and scr_inspect.c called scr_gen_tag, which lives in scr_async.c. Libraries and the runtime's unit tests link those units without scr_async.c, so their links failed on the undefined symbol. Both now read the tag through scr_gen_tag_reader, defined in scr_json.c and set at startup by scr_async.c. A generator capsule only exists when the generator runtime is linked, so the pointer is always set when read. --- packages/runtime/src/scr_async.c | 4 ++++ packages/runtime/src/scr_generator.h | 4 ++++ packages/runtime/src/scr_inspect.c | 2 +- packages/runtime/src/scr_json.c | 4 +++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 124304970..583b1f6d0 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -5556,6 +5556,10 @@ const char *scr_gen_tag(const ScrGen *g) { return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator"; } +__attribute__((constructor)) static void scr_gen_tag_reader_init(void) { + scr_gen_tag_reader = scr_gen_tag; +} + void scr_gen_close(ScrGen *g) { scr_gen_slot_reset(&g->ret); scr_gen_resume_return(g); diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index 76d152f8a..c4397e021 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -73,6 +73,10 @@ void scr_gen_set_tag(ScrGen *g, const char *tag); /* An adapter generator prints with the tag of the generator it adapts. */ void scr_gen_copy_tag(ScrGen *g, const ScrGen *from); const char *scr_gen_tag(const ScrGen *g); +/* scr_gen_tag for units that also link without the generator runtime (a + * library links none): the generator runtime sets it at startup, and only + * then can a generator capsule exist. */ +extern const char *(*scr_gen_tag_reader)(const ScrGen *g); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 019f36536..f72d7aca8 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1138,7 +1138,7 @@ static bool scr_insp_is_generator(const ScrDyn *d) { } static ScrStr *scr_insp_generator(const ScrDyn *d) { - const char *tag = scr_gen_tag(d->v.typed_ref.ptr); + const char *tag = scr_gen_tag_reader(d->v.typed_ref.ptr); if (tag[0] == '!') { static const char message[] = "util.inspect of this iterator object is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 206c01839..6d931d621 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1678,6 +1678,8 @@ ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key) { * later, and it neither grows nor owns heap references. */ static ScrDyn scr_dyn_small_ints[384]; +const char *(*scr_gen_tag_reader)(const ScrGen *g) = NULL; + __attribute__((constructor)) static void scr_dyn_small_ints_init(void) { for (int i = 0; i < 384; i++) { scr_dyn_small_ints[i].rc = SIZE_MAX; @@ -6228,7 +6230,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { if (d->kind == SCR_DYN_TYPED_REF) { if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14); if (scr_dyn_generator(d)) { - const char *gen_tag = scr_gen_tag(d->v.typed_ref.ptr); + const char *gen_tag = scr_gen_tag_reader(d->v.typed_ref.ptr); if (gen_tag[0] == '!') { static const char message[] = "Object.prototype.toString of this iterator is not supported yet"; scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1); From 8802765a1c2bc457517e5fed22cf8855874be3be Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:19:31 -0500 Subject: [PATCH 60/67] Destroy an idle pooled http socket that receives data Node 24's http.Agent installs a read guard on each socket it puts in freeSockets: bytes that arrive while the socket is idle destroy it, so they are never read as the response to the next request that reuses the socket. scriptc discarded the bytes and kept the socket in the pool, so the next request reused it (reusedSocket true, one connection) where Node dials a new one. The client connection's data hook now destroys the socket when it is in an agent's free list. The socket leaves freeSockets and totalSocketCount, and the next request through the agent opens a new connection, as in Node. Corpus 6200 covers an idle socket that receives a stray response. --- .../6200-http-agent-free-socket-data.ts.json | 6 ++ packages/runtime/src/scr_http.c | 7 +++ .../6200-http-agent-free-socket-data.ts | 57 +++++++++++++++++++ tests/harness/corpus-costs.json | 2 + 4 files changed, 72 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json create mode 100644 tests/corpus/6200-http-agent-free-socket-data.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json new file mode 100644 index 000000000..e8df3e384 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-http-agent-free-socket-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6200-http-agent-free-socket-data.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_http.c b/packages/runtime/src/scr_http.c index f34cc18bb..83a1b3583 100644 --- a/packages/runtime/src/scr_http.c +++ b/packages/runtime/src/scr_http.c @@ -3323,6 +3323,13 @@ static void scr_http_conn_pump(ScrHttpConn *conn) { static void scr_http_conn_data(void *ctx, const char *buf, size_t n) { ScrHttpConn *conn = (ScrHttpConn *)ctx; + if (conn->pool_agent != NULL) { + /* Data on an idle pooled socket: the agent destroys the socket, as + * Node's free-socket read guard does, so the bytes are never read as + * the next request's response. */ + scr_net_sock_destroy(conn->sock); + return; + } if (conn->len + n > conn->cap) { size_t cap = conn->cap ? conn->cap : 8192; while (cap < conn->len + n) cap *= 2; diff --git a/tests/corpus/6200-http-agent-free-socket-data.ts b/tests/corpus/6200-http-agent-free-socket-data.ts new file mode 100644 index 000000000..69e89de49 --- /dev/null +++ b/tests/corpus/6200-http-agent-free-socket-data.ts @@ -0,0 +1,57 @@ +// Bytes that arrive on an idle pooled socket are not read as the next +// response: the agent destroys the socket, and the next request through +// the agent dials a new connection. +import * as http from "node:http"; +import * as net from "node:net"; + +let conns = 0; +const server = net.createServer((sock) => { + const id = ++conns; + let buf = ""; + sock.on("data", (d: Buffer) => { + buf += d.toString(); + while (buf.includes("\r\n\r\n")) { + const head = buf.slice(0, buf.indexOf("\r\n\r\n")); + buf = buf.slice(buf.indexOf("\r\n\r\n") + 4); + const path = head.split(" ")[1]!; + const body = `real ${path} conn${id}`; + sock.write(`HTTP/1.1 200 OK\r\nContent-Length: ${body.length}\r\nKeep-Alive: timeout=5\r\n\r\n${body}`); + if (path === "/poisoned") { + setTimeout(() => sock.write("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nPOISON"), 50); + } + } + }); + sock.on("close", () => console.log(`server conn${id} closed`)); + sock.on("error", () => {}); +}); + +const agent = new http.Agent({ keepAlive: true }); +function get(port: number, path: string): Promise { + return new Promise((resolve) => { + const req = http.get({ host: "127.0.0.1", port, path, agent }, (res) => { + let body = ""; + res.on("data", (c: Buffer) => (body += c.toString())); + res.on("end", () => resolve(`${body} reused=${req.reusedSocket}`)); + }); + req.on("error", (e: Error) => resolve(`error ${e.message}`)); + }); +} + +function free(): number { + return Object.keys(agent.freeSockets).length; +} + +server.listen(0, "127.0.0.1", async () => { + const port = (server.address() as net.AddressInfo).port; + console.log(await get(port, "/quiet")); + await new Promise((r) => setTimeout(r, 20)); + console.log("free after /quiet", free()); + console.log(await get(port, "/again")); + console.log(await get(port, "/poisoned")); + await new Promise((r) => setTimeout(r, 200)); + console.log("free after stray bytes", free(), "total", agent.totalSocketCount); + console.log(await get(port, "/next")); + console.log("connections", conns); + agent.destroy(); + server.close(() => console.log("server closed")); +}); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 10d9c3aca..8e3cbeaf2 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2264,6 +2264,7 @@ "6154-cjs-require-nested-destructure/main.js": 2800, "6155-cjs-export-table-methods/main.js": 2600, "6156-cjs-function-members-from-ts/main.ts": 3000, + "6200-http-agent-free-socket-data.ts": 3846, "700-classes-basic.ts": 2530, "701-classes-composition.ts": 2357, "702-classes-this-capture.ts": 2140, @@ -5344,6 +5345,7 @@ "6154-cjs-require-nested-destructure/main.js": 6400, "6155-cjs-export-table-methods/main.js": 6000, "6156-cjs-function-members-from-ts/main.ts": 7000, + "6200-http-agent-free-socket-data.ts": 3452, "700-classes-basic.ts": 2674, "701-classes-composition.ts": 2606, "702-classes-this-capture.ts": 2403, From f75b7d8aabc07daa8095ffb96542959557d02569 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:22:05 -0500 Subject: [PATCH 61/67] Use a computed test name as given when the function's name is unknown A test name computed at run time took the function's name when it was empty, which needs that name at compile time. For a function the program received as a parameter, such as a helper that registers `test(\`case ${label}\`, body)` for its callers, the call was refused, where it compiled before. The name is now used as given in that case, and the limitations page notes that an empty one stays empty, where Node uses the function's name. The test-name-forms fixture registers tests through such a helper and compares them with Node, plain and with SCRIPTC_SAN=1. Without this change it fails to compile. --- docs/content/limitations/builtin-apis/test-runner.mdx | 1 + packages/compiler/src/frontend/lowering/lower-test.ts | 6 ++++++ tests/fixtures/node-test/cases/test-name-forms.ts | 11 ++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 5e368cc03..70143a0c3 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -5,3 +5,4 @@ - **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named ``. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged. - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. - **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. +- **Test names:** a test without a name, or with an empty one, takes its function's name or ``, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`. diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index bb525e02d..c31e674af 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -409,6 +409,12 @@ function lowerTestName( if (!nameNode) return strLit(fallback(), loc); const name = lowerer.lowerExprExpecting(nameNode, STRING); if (name.kind === "strLit") return name.value === "" ? strLit(fallback(), loc) : name; + // A name computed at run time: when the function's own name is not known + // at compile time (a function passed in as a parameter, say), the name + // is used as given, and an empty one stays empty (the limitations page + // notes it). Refusing here would refuse every helper that registers + // tests for its callers. + if (fnNode && staticFunctionName(lowerer, fnNode) === null) return name; const slot = lowerer.declareHiddenLocal("%testName", STRING); const value = varRef(slot.id, STRING, loc); return { diff --git a/tests/fixtures/node-test/cases/test-name-forms.ts b/tests/fixtures/node-test/cases/test-name-forms.ts index 915d68a79..b2f61dd89 100644 --- a/tests/fixtures/node-test/cases/test-name-forms.ts +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -1,6 +1,7 @@ // The (name?, options?, fn?) forms of test, it, describe and t.test, and // Node's default names: a missing or empty name takes the function's name, -// or "". With both skip and todo, skip wins, in either order and +// or "". A helper may register tests with computed names and +// functions it receives as parameters. With both skip and todo, skip wins, in either order and // against the todo method twins. import { describe, it, test } from "node:test"; @@ -8,6 +9,14 @@ function declaredTest(): void {} const boundArrow = (): void => {}; const emptyName = process.argv.length > 99 ? "never" : ""; +// A helper that registers tests for its callers: the name is computed and +// the function arrives as a parameter. +function registerCase(label: string, body: () => void): void { + test(`case ${label}`, body); +} +registerCase("one", () => {}); +registerCase("two", declaredTest); + test(function namedExpression() {}); test(() => {}); test(declaredTest); From d8d48e1637c1161d1f87ffc4939ae3de765565aa Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:31:48 -0500 Subject: [PATCH 62/67] Commit default-region memory as ProgramLifetime instances use it A thread's default region asked for huge pages from its first chunk and gave every class a 16 KiB segment, so in a program that only references LifetimeRegion each thread committed whole 2 MiB pages and per-class segment space for a few instances of each class. Default regions now use small pages, and a class's segments in a default region start at 1 KiB and double up to 16 KiB. Freeable regions keep their page and segment policy. --- packages/runtime/src/scr_region.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index 31d01296e..cc094ef0b 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -65,7 +65,14 @@ void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((we #endif #define SCR_RGN_HDR ((size_t)64) +/* A class's segments in a default region start at SCR_RGN_SEG_FIRST bytes + * and double up to SCR_RGN_SEG: a class with a few instances in a thread's + * default region, as most classes have in most threads, then leaves little + * unused memory behind its last instance. Other regions start at + * SCR_RGN_SEG: their classes are usually many and short-lived, and more + * segments cost the release walk. */ #define SCR_RGN_SEG ((size_t)16384) +#define SCR_RGN_SEG_FIRST ((size_t)1024) #define SCR_RGN_AHEAD ((size_t)256 << 10) /* see scr_rgn_ahead */ #define SCR_RGN_AHEAD_START ((size_t)1 << 20) /* see scr_rgn_ahead */ _Static_assert(sizeof(ScrRgnChunk) <= SCR_RGN_HDR, "the chunk header fits before the first segment"); @@ -82,9 +89,11 @@ _Static_assert(sizeof(ScrRgnChunk) <= SCR_RGN_HDR, "the chunk header fits before #define SCR_RGN_CACHE_BYTES ((size_t)8 << 20) #define SCR_RGN_CACHE_WARM ((size_t)256 << 10) -/* Regions start with small pages and switch to huge pages once they span - * this many chunks (a default region at once): small regions then do not - * pay a 2 MiB fault, large ones do not pay 512 small ones. */ +/* Freeable regions start with small pages and switch to huge pages once + * they span this many chunks: small regions then do not pay a 2 MiB fault, + * large ones do not pay 512 small ones. Default regions keep small pages: + * every thread has one, most hold few instances, and a huge page commits + * 2 MiB per thread for them. */ #define SCR_RGN_HUGE_AFTER 4 /* Retries. A condemned region is attempted at the first retry after its @@ -340,7 +349,7 @@ static void scr_rgn_chunk_unmap(ScrRgnChunk *c) { } /* Whether the region's next chunk uses huge pages. */ -static bool scr_rgn_huge(const ScrRegion *r) { return r->id == 0 || r->nchunks + 1 >= SCR_RGN_HUGE_AFTER; } +static bool scr_rgn_huge(const ScrRegion *r) { return r->id != 0 && r->nchunks + 1 >= SCR_RGN_HUGE_AFTER; } /* A chunk for `r`'s next one, with its page policy. */ static ScrRgnChunk *scr_rgn_chunk_reserve(const ScrRegion *r) { @@ -479,9 +488,11 @@ static void scr_rgn_chunk_retire(ScrRgnThread *t, ScrRgnChunk *c) { /* The instance stride: the emitted fast path rounds the size the same way. */ static inline size_t scr_rgn_stride(const ScrRgnClass *cls) { return (cls->size + 7) & ~(size_t)7; } -static ScrRgnSeg *scr_rgn_seg_new(ScrRegion *r, const ScrRgnClass *cls) { +static ScrRgnSeg *scr_rgn_seg_new(ScrRegion *r, const ScrRgnClass *cls, const ScrRgnSeg *prev) { size_t stride = scr_rgn_stride(cls); - size_t need = stride > SCR_RGN_SEG ? stride : SCR_RGN_SEG; + size_t size = prev ? (size_t)(prev->limit - prev->start) * 2 : r->id == 0 ? SCR_RGN_SEG_FIRST : SCR_RGN_SEG; + if (size > SCR_RGN_SEG) size = SCR_RGN_SEG; + size_t need = stride > size ? stride : size; if (need > SCR_RGN_CHUNK - SCR_RGN_HDR) scr_trap("scriptc: ProgramLifetime instance too large for a region\n"); ScrRgnChunk *c = r->chunks; if (!c || (size_t)((char *)c + SCR_RGN_CHUNK - c->bump) < need) c = scr_rgn_chunk_new(r); @@ -589,7 +600,7 @@ void *scr_region_alloc(ScrRgnCursor *cursor, ScrRgnClass *cls) { if (s && cursor->bump) s->end = cursor->bump; size_t stride = scr_rgn_stride(cls); if (!s || (size_t)(s->limit - s->end) < stride) { - s = scr_rgn_seg_new(r, cls); + s = scr_rgn_seg_new(r, cls, s); r->cur[slot] = s; } char *o = s->end; From 0523806107f5c980971e12a5a678b5f81050d9fd Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 03:37:40 -0500 Subject: [PATCH 63/67] Inspect class instances with added properties as Node does console.log and util.inspect of a class instance list the properties added to it after construction, after its declared fields. JavaScript classes print their own properties in creation order: a field first assigned in a method or under a condition appears only once it exists. Such instances render through their checked-dynamic view when the class has a property bag or is a JavaScript class; other classes keep the generated inspect helper. The runtime inspector detects cycles through the class instances it reaches through views, so a cycle prints [Circular *N] instead of expanding to the depth limit. A circular target's prefix counts toward the 80-column line width, as in Node. Capsules of a recursive class type carry the class's view operations, so a nested instance prints with its class name. --- .../src/backend/llvm/expr-stream-bridges.ts | 9 +- .../src/frontend/lowering/lower-inspect.ts | 70 +++++++++-- .../compiler/src/ir/builtin-signatures.ts | 2 +- ...002-inspect-class-added-properties.ts.json | 6 + .../6003-inspect-js-class-properties.js.json | 6 + packages/runtime/src/scr_inspect.c | 50 ++++++-- packages/runtime/src/scr_inspect.h | 4 +- .../6002-inspect-class-added-properties.ts | 117 ++++++++++++++++++ .../6003-inspect-js-class-properties.js | 40 ++++++ tests/harness/corpus-costs.json | 4 + 10 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json create mode 100644 tests/corpus/6002-inspect-class-added-properties.ts create mode 100644 tests/corpus/6003-inspect-js-class-properties.js diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 76ef35358..8a53b5fa8 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -708,6 +708,10 @@ export function streamTypedRefMaterializeAdapter( } // Publish both symbols before their bodies: class-array and callback // converters can request this same adapter while emitting the commit. + // A recursive field type binds capsules of this class there too, so the + // class view ops are published first. + const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined; + if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`; adapter.commit = `@${snapshot}_commit`; adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot); const B = new BlockBuilder(); @@ -719,10 +723,7 @@ export function streamTypedRefMaterializeAdapter( `llvm emitter bug: typed-ref materialize of unknown class ${t.className}`, ); } - if (liveClassOpsEligible(meta)) { - adapter.classOps = `@${snapshot}_cops`; - liveClassOps(host, snapshot, meta); - } + if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); if (meta.hierarchy && meta.children.length) { const vtSlot = B.tmp(), vt = B.tmp(), diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 2551031eb..37233bcdc 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -20,6 +20,9 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + classDynViewSupported, + DYN_CLASS_PROPERTIES, + isClassOwnEnumerableFieldName, isDirentShape, isDynTypedRefType, recordTextCodecClass, @@ -220,9 +223,10 @@ function inspectSupport( ? null : "error subclasses with declared fields have no inspect lowering yet (the extra own properties are runtime-dynamic)"; } - // A class with subclasses renders through its checked-dynamic view, - // which names the instance's own class and holds its own fields. - if (info.subclasses.length > 0) { + // A class with subclasses, or a JavaScript class, renders through its + // checked-dynamic view, which names the instance's own class and holds + // its own properties. + if (classInspectsThroughView(lowerer, info)) { if (!isDynTypedRefType(t) || visiting.has(t.className)) return null; visiting.add(t.className); const hierarchy = (current: ClassInfo): ClassInfo[] => [ @@ -374,7 +378,7 @@ export function inspectExpr( loc, }; } - if (lowerer.classes.get(t.className)?.subclasses.length) { + if (classInspectsThroughView(lowerer, lowerer.classes.get(t.className))) { return { kind: "libCall", fn: "insp.dyn", @@ -475,6 +479,40 @@ function typeConstituents(lowerer: Lowerer, u: IrType): IrType[] { } } +/** Whether instances of a class render through its checked-dynamic view + * instead of a helper over the declared layout. The view names the + * instance's own class, which a base-typed value may not know statically, + * and lists own properties in creation order. A JavaScript class needs that + * order: its fields can be created conditionally, outside the constructor, + * or by writes to the instance after construction. */ +function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined): boolean { + if (info === undefined) return false; + if (info.subclasses.length > 0) return true; + return ( + info.decl != null && + isJsSourceFile(info.decl.getSourceFile()) && + classDynViewSupported( + info.def.fields, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); +} + +/** Whether a class holds properties added outside its declared layout, which + * only its checked-dynamic view can list. Decided after lowering, when the + * class has its final fields. */ +function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean { + return ( + info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) && + classDynViewSupported( + info.def.fields, + (id) => lowerer.shapes.get(id), + (id) => lowerer.unions.get(id), + ) + ); +} + /* ── the interned per-type helper ────────────────────────────────────── */ /** `%util.insp.(v, r, d): string` — formatValue over the static type: @@ -506,8 +544,8 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // CYCLE-CAPABLE composites (typeReachesItself) run Node's circular // machinery: circCheck first (a value already on the traversal stack // renders [Circular *N] — before the empty/depth answers, Node's - // order), seen-push after begin, and refWrap around end's result (the - // prefix on values the walk found circular). Acyclic types + // order), seen-push after begin, and refWrap around end's result (end + // adds the prefix on values the walk found circular). Acyclic types // keep the zero-cost path. const onCycle = (t.kind === "record" || t.kind === "array" || t.kind === "map" || t.kind === "object") && @@ -1196,7 +1234,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // #private fields never render: they are not properties in any // observable way — Node's inspect omits them entirely (verified), // so a class whose only fields are private prints as `C {}`. - const visible = info.def.fields.filter((f) => !f.name.startsWith("#")); + const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name)); if (visible.length === 0) { body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))]; break; @@ -1316,6 +1354,24 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { body, loc, }; + const inspected = t.kind === "object" ? lowerer.classes.get(t.className) : undefined; + if (inspected !== undefined) { + // Dynamic stores add a property bag to the class after this helper was + // built. The checked-dynamic view lists the bag's properties after the + // declared fields, so a class that has one renders through the view. + lowerer.shapeOrderHelperFinalizers.push(() => { + if (!classInspectsAddedProperties(lowerer, inspected)) return; + fn.body = [ + ret({ + kind: "libCall", + fn: "insp.dyn", + args: [{ kind: "dynFrom", value: v(), liveRef: true, type: DYN, loc }, r(), d()], + type: STRING, + loc, + }), + ]; + }); + } if (rebuildShapeOrderBody !== null) { lowerer.shapeOrderHelperFinalizers.push(() => { rebuildShapeOrderBody!(); diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51765286b..038f591c4 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3888,7 +3888,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /* Circular references over cycle-capable composites (recursive record/ * class types): circCheck answers a value's circular id when it is * already on the traversal stack (0 otherwise), seenPush/refWrap - * bracket the frame (refWrap adds Node's " " prefix to values + * bracket the frame (insp.end adds Node's " " prefix to values * the walk found circular), circular renders "[Circular *N]". */ "insp.circCheck": { argTypes: [null], result: F64 }, "insp.seenPush": { argTypes: [null], result: VOID }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json new file mode 100644 index 000000000..2665f2a27 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6002-inspect-class-added-properties.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6002-inspect-class-added-properties.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json new file mode 100644 index 000000000..d0b03b570 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6003-inspect-js-class-properties.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6003-inspect-js-class-properties.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index f72d7aca8..3194528d3 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -236,6 +236,7 @@ typedef struct { size_t len; size_t cap; bool all_num; /* every entry so far flagged numeric (grouping order) */ + const void *seen; /* the value this frame pushed on the seen stack, or NULL */ } InspFrame; static SCR_TL InspFrame *g_frames; @@ -264,6 +265,10 @@ static void insp_circ_reset(void); /* circular-reference state, below */ /* Begin a non-empty composite: formatRaw's `recurseTimes += 1; * ctx.currentDepth = recurseTimes` plus the uniform +2 the children * format under. `recurse` is the CHILDREN's recursion depth. */ +/* A value whose frame the next scr_insp_begin opens: that frame pushes it + * on the seen stack (the runtime inspector's class instances). */ +static SCR_TL const void *g_pending_seen; + void scr_insp_begin(double recurse) { if (recurse == 1) insp_circ_reset(); /* a fresh top-level value: Node's per-inspect ctx */ if (g_nframes == g_frames_cap) { @@ -276,8 +281,14 @@ void scr_insp_begin(double recurse) { f->len = 0; f->cap = 0; f->all_num = true; + f->seen = NULL; g_cur_depth = recurse; g_indent += 2; + if (g_pending_seen) { + const void *v = g_pending_seen; + g_pending_seen = NULL; + scr_insp_seen_push(v); + } } /* Append one already-rendered entry (borrowed; retained here). is_num @@ -352,6 +363,7 @@ void scr_insp_seen_push(const void *v) { g_seen[g_nseen].ptr = v; g_seen[g_nseen].id = 0; g_nseen++; + if (g_nframes > 0) insp_top()->seen = v; } ScrStr *scr_insp_circular(double id) { @@ -360,17 +372,12 @@ ScrStr *scr_insp_circular(double id) { return scr_str_new(buf, (size_t)n); } +/* Pops the value's seen entry after its frame closed. scr_insp_end already + * wrote the `` prefix of a circular target. */ ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s) { + (void)v; if (g_nseen > 0) g_nseen--; - int id = insp_circ_id(v); - if (id == 0) return scr_str_retain(s); - char buf[32]; - int n = snprintf(buf, sizeof buf, " ", id); - ScrStr *out = scr_str_alloc_raw(s->len + (size_t)n, s->len + (size_t)n); - memcpy(out->data, buf, (size_t)n); - memcpy(out->data + n, s->data, s->len); - out->data[out->len] = '\0'; - return out; + return scr_str_retain(s); } /* formatRaw's Date base: toISOString(), or toString()'s "Invalid Date" @@ -507,6 +514,20 @@ ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse, bool * and Map and Set entries) by their rendered text, in UTF-16 code-unit * order; arrays keep their element order. */ if (g_sorted && !array_extras && f->len > 1) qsort(f->items, f->len, sizeof *f->items, insp_item_cmp); + /* Node adds a circular target's `` to the base before it chooses + * the layout, so the reference counts toward the line width. */ + ScrStr *ref_base = NULL; + int ref_id = f->seen ? insp_circ_id(f->seen) : 0; + if (ref_id) { + char buf[32]; + int n = snprintf(buf, sizeof buf, base->len ? " " : "", ref_id); + size_t len = (size_t)n + base->len; + ref_base = scr_str_alloc_raw(len, len); + memcpy(ref_base->data, buf, (size_t)n); + memcpy(ref_base->data + n, base->data, base->len); + ref_base->data[len] = '\0'; + base = ref_base; + } size_t entries = f->len; bool compact = g_compact >= 1; if (compact && array_extras && entries > 6) insp_group(f, trailing_more); @@ -553,6 +574,7 @@ done:; for (size_t i = 0; i < f->len; i++) scr_str_release(f->items[i]); free(f->items); g_nframes--; + if (ref_base) scr_str_release(ref_base); return ib_take(&out); } @@ -1464,6 +1486,12 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { scr_str_release(body); return ib_take(&out); } + /* A class instance already on the traversal stack renders as + * [Circular *N]. The key is the native instance, the same key the + * compiled inspect helpers push, so a cycle through both resolves. */ + const void *instance = d->v.typed_ref.ptr; + double circular = scr_insp_circ_check(instance); + if (circular > 0) return scr_insp_circular(circular); ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); /* A class instance prints with its class's name, which the checked * view of its fields does not carry. */ @@ -1475,7 +1503,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (!cls) scr_trap("scriptc: out of memory\n"); memcpy(cls, name->data, name->len); cls[name->len] = '\0'; + size_t nseen = g_nseen; + g_pending_seen = instance; out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; free(cls); } else { out = scr_insp_dyn(materialized, recurse, depth); diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 4414330b5..4eb607a5c 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -55,8 +55,8 @@ void scr_insp_entry(ScrStr *s, bool is_num); * FIRST (a value already on the traversal stack answers its circular id, * assigned in discovery order — the caller renders scr_insp_circular and * descends no further), seen_push after begin, and ref_wrap around end's - * result (pops the stack; values in the circular map gain the " " - * prefix). State resets at each top-level value's first frame + * result (pops the stack; scr_insp_end gives values in the circular map + * the " " prefix). State resets at each top-level value's first frame * (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */ double scr_insp_circ_check(const void *v); void scr_insp_seen_push(const void *v); diff --git a/tests/corpus/6002-inspect-class-added-properties.ts b/tests/corpus/6002-inspect-class-added-properties.ts new file mode 100644 index 000000000..a143aa437 --- /dev/null +++ b/tests/corpus/6002-inspect-class-added-properties.ts @@ -0,0 +1,117 @@ +import { inspect } from "node:util"; + +class Point { + x = 1; + y = 2; +} +class Empty {} +class WithCtor { + a: number; + b: string; + constructor(a: number, b: string) { + this.a = a; + this.b = b; + } +} +class Derived extends Point { + w = 4; +} +class Methods { + n = 1; + get double(): number { + return this.n * 2; + } + inc(): void { + this.n++; + } +} + +const p = new Point(); +console.log(p); +console.log(new Empty()); +console.log(new WithCtor(1, "s")); +console.log(new Derived()); +console.log(new Methods()); +console.log([p, p]); +console.log({ p }); +console.log(inspect(p)); +console.log(`${inspect(new Derived())}`); + +class Pair { + x = 1; + y = 2; +} + +class Node2 { + name: string; + next: Node2 | null = null; + constructor(name: string) { + this.name = name; + } +} + +class Holder { + #secret = 1; + label = "h"; + items: number[] = [1, 2, 3]; + inner = new Pair(); +} + +const Anonymous = class { + v = 1; +}; + +const pair = new Pair(); +console.log(pair); +const loose: any = pair; +loose["z"] = 3; +(pair as any).w = "added"; +console.log(pair); +console.log([pair], { pair }); +console.log(inspect(pair, { depth: 0 })); +console.log(inspect({ deep: { deeper: pair } }, { depth: 1 })); + +const a = new Node2("a"); +const b = new Node2("b"); +a.next = b; +b.next = a; +console.log(a); + +const h = new Holder(); +(h as any).extra = { nested: true }; +console.log(h); +console.log(new Anonymous()); +console.log(`${inspect(new Pair())}`); + +const self = new Node2("s"); +self.next = self; +console.log(self); +console.log([a]); +console.log({ ring: a, other: self }); + +class Shape { + label = "shape"; + peer: Shape | null = null; +} +class Circle extends Shape { + r = 1; +} +const c1 = new Circle(); +const c2 = new Circle(); +c1.peer = c2; +c2.peer = c1; +const shapes: Shape[] = [c1]; +console.log(shapes); +console.log(c2); + +class Link { + label = "link"; + peer: Link | null = null; +} +class Tagged extends Link { + tag = 1; +} +const l1 = new Tagged(); +l1.peer = new Tagged(); +const links: Link[] = [l1]; +console.log(links); diff --git a/tests/corpus/6003-inspect-js-class-properties.js b/tests/corpus/6003-inspect-js-class-properties.js new file mode 100644 index 000000000..6a5541841 --- /dev/null +++ b/tests/corpus/6003-inspect-js-class-properties.js @@ -0,0 +1,40 @@ +class Box { + constructor(v) { + this.v = v; + } +} +const b = new Box(1); +console.log(b); +b.extra = "e"; +console.log(b); + +class Plain {} +const q = new Plain(); +console.log(q); +q.a = 1; +q.b = [1, 2]; +console.log(q); +q.nested = new Box(2); +console.log(q); +console.log([q]); + +class Late { + constructor() { + this.a = 1; + } + setB() { + this.b = 2; + } +} +const late = new Late(); +console.log(late); +late.setB(); +console.log(late); + +class Cond { + constructor(flag) { + if (flag) this.x = 1; + this.y = 2; + } +} +console.log(new Cond(true), new Cond(false)); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 10d9c3aca..4f22ed466 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2224,6 +2224,8 @@ "600-closures-basic.ts": 2349, "6000-json-stringify-class-instances.ts": 7567, "6001-json-stringify-js-class-instances.js": 4668, + "6002-inspect-class-added-properties.ts": 6044, + "6003-inspect-js-class-properties.js": 2752, "601-closures-loops.ts": 1788, "602-closures-identity-recursion.ts": 2372, "6020-class-extends-function-constructor.js": 4300, @@ -5304,6 +5306,8 @@ "600-closures-basic.ts": 2534, "6000-json-stringify-class-instances.ts": 12107, "6001-json-stringify-js-class-instances.js": 7469, + "6002-inspect-class-added-properties.ts": 9670, + "6003-inspect-js-class-properties.js": 4403, "601-closures-loops.ts": 2297, "602-closures-identity-recursion.ts": 2427, "6020-class-extends-function-constructor.js": 7500, From ef394d0de9c9e7ee821ef05c512d09b68695078b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 04:09:13 -0500 Subject: [PATCH 64/67] Name the destination of every ownership transfer in the LLVM emitter moveTemp, retainValue and releaseValue are replaced by forms that state where a reference goes or comes from: toHeap and toFrame, retainHeap and retainFrame, releaseHeap and releaseFrame. A heap reference is one that heap memory holds: a field, an array or map element, a capture box, a union box, a module global, a promise or generator slot, the exception cell or another runtime structure. A frame reference is one that a local, a parameter, a return value, a stack object or a temporary holds. Sites whose destination depends on the binding (a local or a module global) or on the function (a coroutine's return fulfills its promise) choose at emission time. adoptHeap marks the runtime results whose +1 the runtime made from heap memory and that become frame values: capture box reads, generator in and out slots, and await results. The statement-frame release is renamed releaseTemps. Both forms of each operation emit what the old call emitted, so the generated IR is unchanged. --- .../src/backend/llvm/dense-array-access.ts | 8 +- packages/compiler/src/backend/llvm/emitter.ts | 172 ++++++++++++------ .../compiler/src/backend/llvm/expr-async.ts | 24 +-- .../compiler/src/backend/llvm/expr-calls.ts | 60 +++--- .../src/backend/llvm/expr-containers.ts | 10 +- .../compiler/src/backend/llvm/expr-control.ts | 28 +-- .../compiler/src/backend/llvm/expr-dynamic.ts | 14 +- .../src/backend/llvm/expr-primitives.ts | 31 ++-- .../compiler/src/backend/llvm/expr-records.ts | 12 +- packages/compiler/src/backend/llvm/lib-abi.ts | 2 +- .../compiler/src/backend/llvm/lib-connect.ts | 2 +- .../compiler/src/backend/llvm/lib-datagram.ts | 12 +- .../compiler/src/backend/llvm/lib-dispatch.ts | 16 +- .../src/backend/llvm/lib-filesystem.ts | 20 +- .../compiler/src/backend/llvm/lib-http2.ts | 6 +- .../compiler/src/backend/llvm/lib-lookup.ts | 2 +- .../compiler/src/backend/llvm/lib-network.ts | 42 ++--- .../compiler/src/backend/llvm/lib-process.ts | 34 ++-- .../compiler/src/backend/llvm/lib-stream.ts | 26 +-- .../compiler/src/backend/llvm/lib-test.ts | 14 +- packages/compiler/src/backend/llvm/lib-tls.ts | 4 +- .../src/backend/llvm/local-array-reads.ts | 4 +- .../src/backend/llvm/local-union-storage.ts | 14 +- .../src/backend/llvm/map-number-lookup.ts | 4 +- .../compiler/src/backend/llvm/split-loops.ts | 4 +- .../src/backend/llvm/stack-objects.ts | 2 +- .../src/backend/llvm/string-slices.ts | 2 +- .../compiler/src/backend/llvm/thread-slots.ts | 8 +- 28 files changed, 318 insertions(+), 259 deletions(-) diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index f19073b75..d02d7f7c3 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -257,7 +257,7 @@ export function emitDenseSlotUnlink( B.condBr(same, swap, done); B.startBlock(swap); B.line(`store ptr ${placeholder}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`); - host.releaseValue(current, element); + host.releaseHeap(current, element); B.br(done); B.startBlock(done); } @@ -315,7 +315,7 @@ export function emitDenseArrayGet( fastLabel = B.newLabel("arr.get.ref"); B.condBr(nonnull, fastLabel, slow); B.startBlock(fastLabel); - fast = retain ? host.retainValue(raw, element) : raw; + fast = retain ? host.retainFrame(raw, element) : raw; } B.br(join); B.startBlock(slow); @@ -493,7 +493,7 @@ export function emitDenseArraySet( const releaseLabel = B.newLabel("arr.set.release"); B.condBr(hadValue, releaseLabel, join); B.startBlock(releaseLabel); - host.releaseValue(old, element); + host.releaseHeap(old, element); } B.br(join); // Below the bound the slot holds a present value: replace it, leaving @@ -508,7 +508,7 @@ export function emitDenseArraySet( ); } storeValue(filledValuePtr); - if (filledOld) host.releaseValue(filledOld, element); + if (filledOld) host.releaseHeap(filledOld, element); B.br(join); B.startBlock(slow); call(" cold"); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 48012704b..7e7e70df6 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -4962,8 +4962,8 @@ export class LlEmitter { } } - /** Strike a refcounted temp from its frame: ownership is being moved. */ - moveTemp(v: LlValue): void { + /** Strike a refcounted temp from its frame. */ + private disownTemp(v: LlValue): void { if (!isRefCounted(v.type) || this.isImmortal(v.name)) return; for (let i = this.frames.length - 1; i >= 0; i--) { const idx = this.frames[i]!.findIndex((e) => e.name === v.name); @@ -4977,23 +4977,69 @@ export class LlEmitter { ); } + /* Ownership transfers name their destination. A heap reference is one + * that heap memory holds: a field, an array or map element, a capture + * box, a union box, a module global, a promise or generator slot, the + * exception cell, or any other runtime structure. A frame reference is + * one that a local, a parameter, a return value, a stack object or a + * temporary holds. Both forms of each operation emit the same code. */ + + /** Ends a temp's frame ownership: its reference moves into heap memory. */ + toHeap(v: LlValue): void { + this.disownTemp(v); + } + + /** Ends a temp's frame ownership: its reference moves to another frame + * owner (a local or parameter slot, a callee's parameter, a return + * value, a stack object, another temp). */ + toFrame(v: LlValue): void { + this.disownTemp(v); + } + /** The retained (+1) read of a refcounted value — type-directed through * the `_v` table (immortals skip, exactly the C retain calls). */ - retainValue(name: string, type: IrType): string { + private retainCall(name: string, type: IrType): string { if (this.isImmortal(name)) return name; const t = this.B.tmp(); this.B.line(`${t} = call ptr ${retainSym(this.shapeHost, type)}(ptr ${name})`); return t; } + /** A +1 of `name` that heap memory keeps. */ + retainHeap(name: string, type: IrType): string { + return this.retainCall(name, type); + } + + /** A +1 of `name` that a frame owns. */ + retainFrame(name: string, type: IrType): string { + return this.retainCall(name, type); + } + /** The release call for one owned refcounted value — type-directed like * releaseCallC (all runtime releases are NULL-tolerant). */ - releaseValue(name: string, type: IrType): void { + private releaseCall(name: string, type: IrType): void { if (this.isImmortal(name)) return; this.B.line(`call void ${releaseSym(this.shapeHost, type)}(ptr ${name})`); } - releaseFrame(frame: LlValue[]): void { + /** Releases a reference that heap memory held. */ + releaseHeap(name: string, type: IrType): void { + this.releaseCall(name, type); + } + + /** Releases a reference that a frame owned. */ + releaseFrame(name: string, type: IrType): void { + this.releaseCall(name, type); + } + + /** A +1 that the runtime made with the heap retain (a box, generator or + * await read) becomes a frame value. Emits nothing; answers `name`. */ + adoptHeap(name: string, _type: IrType): string { + return name; + } + + /** Releases a statement frame's temps and owned slots. */ + releaseTemps(frame: LlValue[]): void { for (const v of frame) { if (v.slot) { const t = this.B.tmp(); @@ -5001,10 +5047,10 @@ export class LlEmitter { if (v.boxed) { this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`); } else { - this.releaseValue(t, v.type); + this.releaseFrame(t, v.type); } } else { - this.releaseValue(v.name, v.type); + this.releaseFrame(v.name, v.type); } } } @@ -5029,7 +5075,7 @@ export class LlEmitter { } else if (e.boxed) { this.B.line(`call void ${boxReleaseSym(this.shapeHost)}(ptr ${t})`); } else { - this.releaseValue(t, e.type); // runtime releases are NULL-tolerant + this.releaseFrame(t, e.type); // runtime releases are NULL-tolerant } } } @@ -5039,7 +5085,7 @@ export class LlEmitter { * everything whose normal fall-through releases the jump bypasses. * Release every scope crossed by the control-flow edge. */ private releaseForJump(frameDepth: number, scopeDepth: number): void { - for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!); + for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!); for (let i = this.scopes.length - 1; i >= scopeDepth; i--) this.releaseScope(this.scopes[i]!); } @@ -5112,7 +5158,7 @@ export class LlEmitter { const target = this.tryStack[this.tryStack.length - 1]; const frameDepth = target?.frameDepth ?? 0; const scopeDepth = target?.scopeDepth ?? 0; - for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseFrame(this.frames[i]!); + for (let i = this.frames.length - 1; i >= frameDepth; i--) this.releaseTemps(this.frames[i]!); let terminator: string; if (target) { target.used = true; @@ -5642,7 +5688,7 @@ export class LlEmitter { if (target.value !== null) { this.frames.push([]); for (const read of speculativeReads(target.value)) reads.set(read, this.emitExpr(read)); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); } this.frames.push([]); let result: string; @@ -5966,7 +6012,7 @@ export class LlEmitter { unionExtract(uName: string, unionId: string, arm: IrType): string { if (arm.kind === "f64" || arm.kind === "procStream") return this.unionGetF64(uName); if (arm.kind === "bool") return this.unionGetBool(uName); - return this.retainValue(this.unionPeek(uName, unionId), arm); + return this.retainFrame(this.unionPeek(uName, unionId), arm); } /** A scalar f64 arm's payload (scr_union_get_f64 inlined). Inline loads @@ -6208,11 +6254,11 @@ export class LlEmitter { * in, and the old value is released AFTER the field is overwritten. */ replaceField(ptr: string, type: IrType, inline: boolean, v: LlValue): void { if (!inline && isRefCounted(type)) { - this.moveTemp(v); + this.toHeap(v); const old = this.B.tmp(); this.B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, v.name, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); return; } this.storeField(ptr, type, v.name, inline); @@ -6220,7 +6266,7 @@ export class LlEmitter { /** Stores `v` (from emitFieldValue) into a fresh field slot. */ initField(ptr: string, type: IrType, inline: boolean, v: LlValue): void { - if (!inline && isRefCounted(type)) this.moveTemp(v); + if (!inline && isRefCounted(type)) this.toHeap(v); this.storeField(ptr, type, v.name, inline); } @@ -6386,7 +6432,7 @@ export class LlEmitter { if (!stack) B.line(`${box} = ${boxNewCall(this.shapeHost, type)}`); const value = B.tmp(); B.line(`${value} = load ${this.llType(type)}, ptr %${mangleLocal(localId)}`); - this.boxSet(box, type, isRefCounted(type) ? this.retainValue(value, type) : value); + this.boxSet(box, type, isRefCounted(type) ? this.retainHeap(value, type) : value); B.line(`store ptr ${box}, ptr ${cache}`); B.br(join); B.startBlock(join); @@ -6532,7 +6578,7 @@ export class LlEmitter { this.boxSet( box, local.type, - borrowed && isRefCounted(local.type) ? this.retainValue(value, local.type) : value, + borrowed && isRefCounted(local.type) ? this.retainHeap(value, local.type) : value, ); } @@ -7557,14 +7603,14 @@ export class LlEmitter { B.line(`${present} = icmp ne ptr %result, null`); B.condBr(present, own, done); B.startBlock(own); - B.line(`store ptr ${this.retainValue("%result", fn.returnType)}, ptr ${slot}`); + B.line(`store ptr ${this.retainFrame("%result", fn.returnType)}, ptr ${slot}`); B.br(done); B.startBlock(done); result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); } for (const index of borrowed) - if (!kept.has(index)) this.releaseValue(`%p${index}`, fn.params[index]!.type); + if (!kept.has(index)) this.releaseFrame(`%p${index}`, fn.params[index]!.type); B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`); const symbol = kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name); @@ -7725,7 +7771,7 @@ export class LlEmitter { private emitAdoptingReturn(s: IrStmt & { kind: "return" }, adopted: ExprOf<"awaitExpr">): void { const B = this.B; const promise = this.emitExpr(adopted.value); - this.moveTemp(promise); + this.toFrame(promise); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ptr ; adopted return promise`); B.line(`store ptr ${promise.name}, ptr ${slot}`); @@ -7759,7 +7805,8 @@ export class LlEmitter { const v = this.emitExpr(s.value!); if (this.adoptedOperand !== null) throw new InternalCompilerError("llvm emitter bug: adopted return promise was not awaited"); - this.moveTemp(v); + if (this.currentCoro !== null) this.toHeap(v); + else this.toFrame(v); this.releaseForJump(0, 0); if (!B.isTerminated()) { if (this.currentCoro !== null) { @@ -8104,7 +8151,7 @@ export class LlEmitter { const union = emitStackUnion(this, localUnion); B.line(`store ptr ${union.value.name}, ptr ${b.slot}`); if (union.payload) { - this.moveTemp(union.payload); + this.toFrame(union.payload); this.scopes[this.scopes.length - 1]!.push({ slot: union.payloadSlot, type: union.payload.type, @@ -8151,7 +8198,7 @@ export class LlEmitter { if (stack.owner) this.scopes[this.scopes.length - 1]!.push(stack.owner); if (s.init) { const value = this.emitExpr(s.init); - if (isRefCounted(value.type)) this.moveTemp(value); + if (isRefCounted(value.type)) this.toFrame(value); this.boxSet(stack.box, b.type, value.name); } break; @@ -8178,7 +8225,7 @@ export class LlEmitter { break; } const v = this.emitExpr(s.init); - if (isRefCounted(v.type)) this.moveTemp(v); // the box takes ownership + if (isRefCounted(v.type)) this.toHeap(v); // the box takes ownership if (b.local!.tdz === true && boxAccess(b.type) !== "ref") { this.tdzScalarInit(box, b.type, v.name); } else { @@ -8236,7 +8283,8 @@ export class LlEmitter { break; } const v = this.emitExpr(s.init); - this.moveTemp(v); + if (b.kind === "global") this.toHeap(v); + else this.toFrame(v); B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); this.storeIntegerView(s.localId, v, s.init); if (isRefCounted(b.type)) { @@ -8259,10 +8307,10 @@ export class LlEmitter { const param = this.binding(s.localId); const slot = param.slot; const v = this.emitExpr(s.value); - this.moveTemp(v); + this.toFrame(v); const old = B.tmp(); B.line(`${old} = load ptr, ptr ${owner}`); - this.releaseValue(old, param.type); + this.releaseFrame(old, param.type); B.line(`store ptr ${v.name}, ptr ${slot}`); B.line(`store ptr ${v.name}, ptr ${owner}`); break; @@ -8289,7 +8337,7 @@ export class LlEmitter { B.line(`${bits} = load ${struct}, ptr ${value.name}`); B.line(`store ${struct} ${bits}, ptr ${target}`); this.disownSlots(owned); - for (const field of old) this.releaseValue(field.name, field.type); + for (const field of old) this.releaseFrame(field.name, field.type); break; } const localStorage = this.localUnionStorage.get(s.localId); @@ -8313,14 +8361,16 @@ export class LlEmitter { if (b.kind === "boxed") { this.writeBindingBox(this.loadBox(b.slot), b.local!, v.name, s.initializes); // The RHS remains frame-owned until a possible TDZ throw passes. - if (isRefCounted(v.type)) this.moveTemp(v); + if (isRefCounted(v.type)) this.toHeap(v); break; } - this.moveTemp(v); + if (b.kind === "global") this.toHeap(v); + else this.toFrame(v); if (isRefCounted(b.type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); - this.releaseValue(old, b.type); + if (b.kind === "global") this.releaseHeap(old, b.type); + else this.releaseFrame(old, b.type); } B.line(`store ${this.llType(b.type)} ${v.name}, ptr ${b.slot}`); this.storeIntegerView(s.localId, v, s.value); @@ -8371,7 +8421,7 @@ export class LlEmitter { throw new InternalCompilerError("llvm emitter bug: arraySet on non-array"); const acc = elemAccess(s.arr.type.elem); this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name); - if (acc === "ref") this.moveTemp(v); + if (acc === "ref") this.toHeap(v); emitDenseArraySet( this, arr.name, @@ -8436,7 +8486,7 @@ export class LlEmitter { if (acc === "ref" && isRefCounted(elem)) { const old = B.tmp(); B.line(`${old} = inttoptr i64 ${raw} to ptr`); - this.releaseValue(old, elem); + this.releaseHeap(old, elem); } B.br(done); B.startBlock(call); @@ -8586,7 +8636,7 @@ export class LlEmitter { const v = this.emitExpr(s.value); if (this.publishedTypes?.recordGuarded(s.shapeId) === true) this.emitPublishedFieldGuard(obj.name, null, "[key]"); - if (isRefCounted(v.type)) this.moveTemp(v); + if (isRefCounted(v.type)) this.toHeap(v); const shape = this.recordShape(s.shapeId); // Signature-free shapes dispatch over their (one-typed) declared // fields and TRAP on a miss (scr_record_key_miss — JS would add @@ -8652,12 +8702,12 @@ export class LlEmitter { if (inline) { // The slot copies the checked value's bits; nv is released below. this.storeField(ptr, type, nv, true); - this.releaseValue(nv, type); + this.releaseFrame(nv, type); } else if (isRefCounted(type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, nv, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); } else { this.storeField(ptr, type, nv, false); } @@ -8687,12 +8737,12 @@ export class LlEmitter { if (inline) { // The write owns the moved-in value; the slot copies its bits. this.storeField(ptr, type, v.name, true); - this.releaseValue(v.name, type); + this.releaseHeap(v.name, type); } else if (isRefCounted(type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); this.storeField(ptr, type, v.name, false); - this.releaseValue(old, type); + this.releaseHeap(old, type); } else { this.storeField(ptr, type, v.name, false); } @@ -8703,7 +8753,7 @@ export class LlEmitter { // The MISS on a fixed shape: release the moved-in value, throw // the catchable TypeError naming the key (scr_record_key_miss — // JS would add the property, the documented divergence). - if (isRefCounted(iv)) this.releaseValue(v.name, iv); + if (isRefCounted(iv)) this.releaseHeap(v.name, iv); this.declare(`declare void @scr_record_key_miss(ptr)`); B.line(`call void @scr_record_key_miss(ptr ${key.name})`); B.br(join); @@ -9346,7 +9396,8 @@ export class LlEmitter { } else v = this.emitReadReceiver(s.value); } else if (s.value !== null) { v = this.emitExpr(s.value); - this.moveTemp(v); + if (this.currentCoro !== null) this.toHeap(v); + else this.toFrame(v); } // An async generator's `return v` awaits v before any crossed // finally runs. Past finally regions that turn is taken here and @@ -9415,7 +9466,7 @@ export class LlEmitter { // unwind unconditionally (the innermost try handler, or out of the // function) — the same release path as return/break/continue. const v = this.emitExpr(s.value); - if (isRefCounted(s.value.type)) this.moveTemp(v); // the cell takes ownership + if (isRefCounted(s.value.type)) this.toHeap(v); // the cell takes ownership this.emitThrowValue({ name: v.name, type: s.value.type }); this.emitUnwind(); break; @@ -9461,7 +9512,7 @@ export class LlEmitter { s.kind !== "rethrow" && s.kind !== "runtimeFence" ) { - this.releaseFrame(frame); + this.releaseTemps(frame); } } @@ -9673,7 +9724,7 @@ export class LlEmitter { } else { B.line(`${hit} = fcmp oeq double ${disc.name}, ${t.name}`); } - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); const next = B.newLabel("sw.t"); B.condBr(hit, caseLabels[i]!, next); B.startBlock(next); @@ -9714,7 +9765,7 @@ export class LlEmitter { private emitCondition(cond: IrExpr): string { const v = this.emitExpr(cond); const frame = this.currentFrame(); - this.releaseFrame(frame); + this.releaseTemps(frame); frame.length = 0; return v.name; } @@ -9726,11 +9777,11 @@ export class LlEmitter { emitBranchInto(slot: string, expr: IrExpr, integerSlot?: string): boolean { this.frames.push([]); const v = this.emitExpr(expr); - this.moveTemp(v); + this.toFrame(v); this.B.line(`store ${this.llType(expr.type)} ${v.name}, ptr ${slot}`); if (integerSlot !== undefined && v.uint32 !== undefined) this.B.line(`store i32 ${v.uint32}, ptr ${integerSlot}`); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); return v.uint32 !== undefined; } @@ -9884,12 +9935,13 @@ export class LlEmitter { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); B.line(`store ptr null, ptr ${b.slot}`); - this.releaseValue(old, b.type); + if (b.kind === "global") this.releaseHeap(old, b.type); + else this.releaseFrame(old, b.type); } let raw: string; if (charCode !== null) { // The snapshot moves into the consuming character append. - this.moveTemp(snapshot); + this.toFrame(snapshot); this.declare(`declare ptr @scr_str_concat_move_char_code(ptr, double)`); raw = B.tmp(); B.line( @@ -9904,11 +9956,15 @@ export class LlEmitter { } const result = this.own({ name: raw, type: left.type }); if (retainForYield) { - const stored = this.retainValue(result.name, result.type); + const stored = + b.kind === "local" + ? this.retainFrame(result.name, result.type) + : this.retainHeap(result.name, result.type); if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, stored); else B.line(`store ptr ${stored}, ptr ${b.slot}`); } else { - this.moveTemp(result); + if (b.kind === "local") this.toFrame(result); + else this.toHeap(result); if (b.kind === "boxed") this.boxSet(this.loadBox(b.slot), b.type, result.name); else B.line(`store ptr ${result.name}, ptr ${b.slot}`); } @@ -9971,7 +10027,7 @@ export class LlEmitter { if (owned) { const old = this.loadField(ptr, fieldType, false); this.storeField(ptr, fieldType, "null", false); - this.releaseValue(old, fieldType); + this.releaseHeap(old, fieldType); } let raw: string; if (right === null) { @@ -10416,7 +10472,7 @@ export class LlEmitter { } // The result IS the operand: ownership moves to the result temp. if (!isRefCounted(value.type)) return { name: value.name, type: e.type }; - this.moveTemp(value); + this.toFrame(value); return this.own({ name: value.name, type: e.type }); } @@ -10669,7 +10725,7 @@ export class LlEmitter { this.frames.push([]); const result = this.emitReadReceiver(value); B.line(`store ptr ${result.name}, ptr ${slot}`); - this.releaseFrame(this.frames.pop()!); + this.releaseTemps(this.frames.pop()!); B.br(join); } B.startBlock(join); @@ -10765,7 +10821,7 @@ export class LlEmitter { const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field); const p = this.B.tmp(); this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); - const owner = this.own({ name: this.retainValue(p, nullable.arm), type: nullable.arm }); + const owner = this.own({ name: this.retainFrame(p, nullable.arm), type: nullable.arm }); return { box: this.stackNullableBox(owner.name, nullable), owner }; } @@ -10804,7 +10860,7 @@ export class LlEmitter { B.line(`store ptr ${this.unitInstanceRef(nullable.unionId, nullable.unitTag)}, ptr ${slot}`); B.br(join); B.startBlock(ref); - const owned = this.retainValue(p, nullable.arm); + const owned = this.retainHeap(p, nullable.arm); const box = this.unionNewOwned(nullable.unionId, nullable.refTag, { name: owned, type: nullable.arm, @@ -10827,7 +10883,7 @@ export class LlEmitter { B.line(`${isRef} = icmp eq i32 ${tag}, ${nullable.refTag}`); const payload = this.unionPeek(u, nullable.unionId); B.line(`${sel} = select i1 ${isRef}, ptr ${payload}, ptr null`); - return this.retainValue(sel, nullable.arm); + return this.retainHeap(sel, nullable.arm); } /** `obj.f = value` for a nullable-pointer field: the new instance pointer @@ -10852,7 +10908,7 @@ export class LlEmitter { ) { if (value.tag === nullable.refTag) { const inner = this.emitExpr(value.value); - this.moveTemp(inner); + this.toHeap(inner); next = inner.name; } else { // Unit payloads carry nothing; a void payload runs for effects. @@ -10868,7 +10924,7 @@ export class LlEmitter { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${ptr}`); B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`); - this.releaseValue(old, nullable.arm); + this.releaseHeap(old, nullable.arm); } /** A union binding whose slot may hold a private stack box: stack locals diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 6731e13c9..6da63977d 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -162,7 +162,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic const p = B.tmp(); B.line(`${p} = call ptr @scr_promise_new()`); const out = host.own({ name: p, type: e.type }); - host.moveTemp(reason); // the cell takes ownership + host.toHeap(reason); // the cell takes ownership host.emitThrowValue({ name: reason.name, type: reasonT }); B.line(`call void @scr_promise_reject_pending(ptr ${p})`); return out; @@ -191,12 +191,12 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic host.declare(`declare void @scr_promise_fulfill_bool(ptr, i1 zeroext)`); B.line(`call void @scr_promise_fulfill_bool(ptr ${p}, i1 ${v.name})`); } else if (t.kind === "string") { - host.moveTemp(v); + host.toHeap(v); host.declare(`declare void @scr_promise_fulfill_str(ptr, ptr)`); B.line(`call void @scr_promise_fulfill_str(ptr ${p}, ptr ${v.name})`); } else { const rc = vAdapters(host.shapeHost, t); - host.moveTemp(v); + host.toHeap(v); host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); B.line( `call void @scr_promise_fulfill_ref(ptr ${p}, ptr ${v.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`, @@ -537,7 +537,7 @@ export function emitAsyncExpr( B.line(`call void @scr_gen_yield_bool(i1 ${v.name})`); } } else { - host.moveTemp(v); // the OUT slot takes ownership + host.toHeap(v); // the OUT slot takes ownership if (host.currentCoro !== null) { const coro = host.currentCoro; const release = vAdapters(host.shapeHost, yt).release; @@ -601,7 +601,7 @@ export function emitAsyncExpr( // Refcounted channels: the slot's +1 moves out. host.declare(`declare ptr @scr_gen_take_in_ref()`); B.line(`${t} = call ptr @scr_gen_take_in_ref()`); - return host.own({ name: t, type: e.type }); + return host.own({ name: host.adoptHeap(t, e.type), type: e.type }); } case "genResume": { // One consumer resume: park the sent value (typed per mode), hop @@ -731,7 +731,7 @@ export function emitAsyncExpr( ); host.declare(`declare ptr @scr_gen_take_out_ref(ptr)`); B.line(`${t} = call ptr @scr_gen_take_out_ref(ptr ${gb.name})`); - return host.own({ name: t, type: e.type }); + return host.own({ name: host.adoptHeap(t, e.type), type: e.type }); } // Valueless: an empty IN slot reads as undefined on any channel. host.declare(`declare zeroext i1 @scr_gen_step(ptr)`); @@ -748,7 +748,7 @@ export function emitAsyncExpr( const g = host.emitExpr(e.gen); // borrowed for the calls below const sendArg = (store: (aName: string, t: IrType) => void): void => { const a = host.emitExpr(e.arg!); - if (isRefCounted(e.arg!.type)) host.moveTemp(a); // the slot takes ownership + if (isRefCounted(e.arg!.type)) host.toHeap(a); // the slot takes ownership store(a.name, e.arg!.type); }; const parkIn = (name: string, t: IrType): void => { @@ -786,7 +786,7 @@ export function emitAsyncExpr( } else { const a = host.emitExpr(e.arg); const t = e.arg.type; - if (isRefCounted(t)) host.moveTemp(a); + if (isRefCounted(t)) host.toHeap(a); if (t.kind === "f64" || t.kind === "date") { host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; @@ -805,7 +805,7 @@ export function emitAsyncExpr( } else { const a = host.emitExpr(e.arg); const t = e.arg.type; - if (isRefCounted(t)) host.moveTemp(a); + if (isRefCounted(t)) host.toHeap(a); if (t.kind === "f64" || t.kind === "date") { host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; @@ -823,7 +823,7 @@ export function emitAsyncExpr( "llvm emitter bug: async genResume throw with no payload", ); const a = host.emitExpr(e.arg); - if (isRefCounted(e.arg.type)) host.moveTemp(a); + if (isRefCounted(e.arg.type)) host.toHeap(a); host.emitThrowValue({ name: a.name, type: e.arg.type }); host.declare(`declare ptr @scr_async_gen_throw(ptr)`); call = `call ptr @scr_async_gen_throw(ptr ${g.name})`; @@ -873,7 +873,7 @@ export function emitAsyncExpr( if (e.arg === null) throw new InternalCompilerError("llvm emitter bug: genResume throw with no payload"); const a = host.emitExpr(e.arg); - if (isRefCounted(e.arg.type)) host.moveTemp(a); // the cell takes ownership + if (isRefCounted(e.arg.type)) host.toHeap(a); // the cell takes ownership host.emitThrowValue({ name: a.name, type: e.arg.type }); host.declare(`declare void @scr_gen_resume_throw(ptr)`); B.line(`call void @scr_gen_resume_throw(ptr ${g.name})`); @@ -955,7 +955,7 @@ export function emitAsyncExpr( host.declare(`declare ptr @${refAwait}(ptr)`); B.line(`${t} = call ptr @${refAwait}(ptr ${pr.name})`); } - const out = host.own({ name: t, type: e.type }); + const out = host.own({ name: host.adoptHeap(t, e.type), type: e.type }); host.emitPendingCheck(); return out; } diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b1d810a3f..6446a581b 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -165,7 +165,7 @@ export function emitCallExpr( // This also preserves snapshots when a later argument mutates its // source, and releases earlier arguments if evaluation throws. args.forEach((a, index) => { - if (!borrowed?.has(index)) host.moveTemp(a); + if (!borrowed?.has(index)) host.toFrame(a); }); const abi = awaitEntry ? undefined : host.int32Abis.get(e.callee); const argList = args @@ -224,14 +224,14 @@ export function emitCallExpr( if (e.type.kind === "void") { B.line(`call void ${target}(${argList})`); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); if (abi?.union) { B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); if (stackResult) return host.stackScalarUnion(t, e.type); return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type }); } @@ -239,7 +239,7 @@ export function emitCallExpr( B.line(`${t} = call i32 ${target}(${argList})`); const out = host.int32Result(t, e.type); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); - if (borrowed) host.releaseFrame(host.frames.pop()!); + if (borrowed) host.releaseTemps(host.frames.pop()!); return out; } B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`); @@ -249,14 +249,14 @@ export function emitCallExpr( // test the exception first, then own the result before the // argument snapshots that keep it reachable are released. if (host.mayThrow.has(e.callee)) host.emitPendingCheck(); - out = host.own({ name: host.retainValue(t, e.type), type: e.type }); + out = host.own({ name: host.retainFrame(t, e.type), type: e.type }); } else { out = host.own({ name: t, type: e.type }); if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out); } if (borrowed) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -718,7 +718,7 @@ export function emitCallExpr( // every mention yields the same pointer, so `f === f` is true. host.fnValues.add(e.fnName); return host.own({ - name: host.retainValue(`@${mangleFnClosure(e.fnName)}`, e.type), + name: host.retainFrame(`@${mangleFnClosure(e.fnName)}`, e.type), type: e.type, }); } @@ -880,7 +880,7 @@ export function emitCallExpr( lent?.has(i) && lend[i] ? host.emitReadReceiver(a) : host.emitExpr(a), ); args.forEach((a, i) => { - if (!lent?.has(i)) host.moveTemp(a); + if (!lent?.has(i)) host.toFrame(a); }); const argListOf = (values: readonly LlValue[]): string => [ @@ -891,7 +891,7 @@ export function emitCallExpr( const ownedArgs = (): LlValue[] => lent ? args.map((a, i) => - lent.has(i) ? { name: host.retainValue(a.name, a.type), type: a.type } : a, + lent.has(i) ? { name: host.retainFrame(a.name, a.type), type: a.type } : a, ) : args; const invoke = (target: string, needsReceiver: boolean, values = args): string => { @@ -978,7 +978,7 @@ export function emitCallExpr( } if (e.type.kind === "void") { if (host.indirectMayThrow) host.emitPendingCheck(); - if (lent) host.releaseFrame(host.frames.pop()!); + if (lent) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } // The check runs AFTER the result temp joins the frame: an unwind @@ -986,15 +986,15 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.indirectMayThrow) host.emitPendingCheck(); if (lent) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; } case "selfRef": // The running closure itself (env is borrowed; the result owned). - return host.own({ name: host.retainValue("%sc_env", e.type), type: e.type }); + return host.own({ name: host.retainFrame("%sc_env", e.type), type: e.type }); case "new": { // Allocate (fields zeroed, vt stamped), then run the ctor. The // ctor owns and releases its `this` param like any callee, so it @@ -1034,7 +1034,7 @@ export function emitCallExpr( }); return host.own({ name: value, type: e.type }); } - return host.own({ name: host.retainValue(`@${sym}`, e.type), type: e.type }); + return host.own({ name: host.retainFrame(`@${sym}`, e.type), type: e.type }); } case "newValue": { // Construction through a class VALUE: call the class object's @@ -1050,7 +1050,7 @@ export function emitCallExpr( ); const callee = host.emitExpr(e.callee); const args = e.args.map((a) => host.emitExpr(a)); - for (const a of args) host.moveTemp(a); // the constructor owns its params + for (const a of args) host.toFrame(a); // the constructor owns its params const ctorp = B.tmp(); const thunk = B.tmp(); B.line(`${ctorp} = getelementptr inbounds %ScrClassObj, ptr ${callee.name}, i64 0, i32 3`); @@ -1121,7 +1121,7 @@ export function emitCallExpr( // One ScrPromise* either way — ownership transfers, type-only //. const v = host.emitExpr(e.value); - host.moveTemp(v); + host.toFrame(v); return host.own({ name: v.name, type: e.type }); } case "upcast": @@ -1134,7 +1134,7 @@ export function emitCallExpr( const v = host.emitExpr(e.value); if (e.kind === "downcast" && e.type.kind === "object" && v.type.kind === "object") emitDowncastGuard(host, v.name, v.type.className, e.type.className); - if (isRefCounted(v.type)) host.moveTemp(v); + if (isRefCounted(v.type)) host.toFrame(v); return host.own({ name: v.name, type: e.type }); } case "instanceOf": { @@ -1188,7 +1188,7 @@ export function emitCallExpr( borrowed.has(index) && inputs[index] ? host.emitReadReceiver(a) : host.emitExpr(a), ); args.forEach((a, index) => { - if (!borrowed.has(index)) host.moveTemp(a); + if (!borrowed.has(index)) host.toFrame(a); }); const recv = args[0]!.name; const vtp = B.tmp(); @@ -1239,7 +1239,7 @@ export function emitCallExpr( B.startBlock(done); if (e.type.kind === "void") { if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); - if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!); + if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); @@ -1247,8 +1247,8 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); if (borrowed.size > 0) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -1256,7 +1256,7 @@ export function emitCallExpr( if (e.type.kind === "void") { B.line(`call void ${fn}(${argList})`); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); - if (borrowed.size > 0) host.releaseFrame(host.frames.pop()!); + if (borrowed.size > 0) host.releaseTemps(host.frames.pop()!); return { name: "", type: e.type }; } const t = B.tmp(); @@ -1264,8 +1264,8 @@ export function emitCallExpr( const out = host.own({ name: t, type: e.type }); if (host.mayThrowMethods.has(e.method)) host.emitPendingCheck(); if (borrowed.size > 0) { - host.moveTemp(out); - host.releaseFrame(host.frames.pop()!); + host.toFrame(out); + host.releaseTemps(host.frames.pop()!); return host.own(out); } return out; @@ -1392,7 +1392,7 @@ export function emitConstruction( return host.emitExpr(a); }); args.forEach((a, i) => { - if (!borrowed.has(i + 1)) host.moveTemp(a); + if (!borrowed.has(i + 1)) host.toFrame(a); }); const self = borrowed.has(0) ? o @@ -1408,11 +1408,11 @@ export function emitConstruction( `call void @${mangleBorrowedFunction(ctorName)}${host.int32Abis.has(ctorName) ? INT32_ABI_SUFFIX : ""}(${argList})`, ); if (host.mayThrow.has(ctorName)) host.emitPendingCheck(); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); return; } const args = e.args.map((a) => host.emitExpr(a)); - for (const a of args) host.moveTemp(a); + for (const a of args) host.toFrame(a); const r = B.tmp(); B.line(`${r} = call ptr @${mangleClassRetain(e.className)}(ptr ${o})`); const argList = [`ptr ${r}`, ...ctorArguments(host, ctorName, ctor, args, e.args)].join(", "); @@ -1454,8 +1454,8 @@ function emitNumericIndexRead( B.startBlock(slow); host.frames.push([]); const read = emitCallExpr(host, e, false, false, undefined, true); - host.moveTemp(read); - host.releaseFrame(host.frames.pop()!); + host.toFrame(read); + host.releaseTemps(host.frames.pop()!); const slowLabel = B.currentLabel(); B.terminate(`br label %${join}`); B.startBlock(join); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index 22a0b4df5..a04f46f7b 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -118,7 +118,7 @@ export function emitArrayValues( sharedBuffer?: string, ): string { const B = host.B; - if (acc === "ref") values.forEach((value) => host.moveTemp(value)); + if (acc === "ref") values.forEach((value) => host.toHeap(value)); if (values.length === 0) { host.declare("declare double @scr_arr_len(ptr)"); const result = B.tmp(); @@ -219,7 +219,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { B.startBlock(label); host.frames.push([]); emitDiscarded(host, arm); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(lj); } B.startBlock(lj); @@ -235,7 +235,7 @@ function emitDiscarded(host: LlvmEmitterContext, e: IrExpr): void { const start = host.emitExpr(e.args[0]!); const count = e.args[1] ? host.emitExpr(e.args[1]).name : F64_INF; const values = items.map((item) => host.emitExpr(item)); - if (acc === "ref") values.forEach((value) => host.moveTemp(value)); + if (acc === "ref") values.forEach((value) => host.toHeap(value)); let buffer = "null"; if (values.length > 0) { buffer = B.tmp(); @@ -1069,7 +1069,7 @@ export function emitMapNew(host: LlvmEmitterContext, e: IrExpr & { kind: "mapNew for (const pair of e.seed ?? []) { const k = emitBorrowedInput(host, pair.key); const v = host.emitExpr(pair.value); - if (vAcc === "ref") host.moveTemp(v); // the value MOVES in + if (vAcc === "ref") host.toHeap(v); // the value MOVES in host.mapSet(m, kAcc, vAcc, k.name, v.name); } return out; @@ -1370,7 +1370,7 @@ export function emitMapLikeIntrinsic( const k = emitBorrowedInput(host, e.args[0]!); const v = host.emitExpr(e.args[1]!); const vAcc = elemAccess(receiverType.value); - if (vAcc === "ref") host.moveTemp(v); + if (vAcc === "ref") host.toHeap(v); const ordinary = (): void => host.mapSet(r.name, kAcc, vAcc, k.name, v.name); if (kAcc === "f64" && vAcc !== "ref" && host.readNumberMapWrites.has(e)) { const bits = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts index fd4bc8787..fd2061aae 100644 --- a/packages/compiler/src/backend/llvm/expr-control.ts +++ b/packages/compiler/src/backend/llvm/expr-control.ts @@ -58,7 +58,7 @@ export function emitControlExpr( // and the right runs in its own frame. const ty = host.llType(e.type); const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); B.line(`store ${ty} ${l.name}, ptr ${slot}`); @@ -73,7 +73,7 @@ export function emitControlExpr( if (e.op === "&&") B.condBr(truthy, rightLabel, joinLabel); else B.condBr(truthy, joinLabel, rightLabel); B.startBlock(rightLabel); - if (isRefCounted(e.type)) host.releaseValue(l.name, e.type); + if (isRefCounted(e.type)) host.releaseFrame(l.name, e.type); const rightInteger = host.emitBranchInto(slot, e.right, integerSlot); B.br(joinLabel); B.startBlock(joinLabel); @@ -157,7 +157,7 @@ export function emitControlExpr( host.chainSlots.set(e.id, { name: bind, type: e.receiver.type, slot: true }); host.frames.push([]); host.emitExpr(e.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(e.id); B.br(lj); B.startBlock(lj); @@ -203,7 +203,7 @@ export function emitControlExpr( bindPresent(); host.frames.push([]); host.emitExpr(e.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(e.id); B.br(lj); B.startBlock(lj); @@ -272,7 +272,7 @@ export function emitControlExpr( const v = B.tmp(); B.line(`${v} = load ${host.llType(bound.type)}, ptr ${bound.name}`); if (!isRefCounted(e.type)) return { name: v, type: e.type }; - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); } case "orDefault": { // `u || d` narrowed to the single non-unit arm: nullish's dance @@ -309,7 +309,7 @@ export function emitControlExpr( return host.own({ name: t, type: e.type }); } const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const ty = host.llType(e.type); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); @@ -337,11 +337,11 @@ export function emitControlExpr( B.line( `store ${ty} ${host.unionExtract(l.name, e.left.type.unionId, e.type)}, ptr ${slot}`, ); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); } B.br(lj); B.startBlock(lf); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lj); @@ -359,7 +359,7 @@ export function emitControlExpr( // The native checked-value kind decides whether the left is // nullish; the right runs lazily in its branch. const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); host.declare(`declare zeroext i1 @scr_dyn_is_nullish(ptr)`); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ptr`); @@ -370,7 +370,7 @@ export function emitControlExpr( const lj = B.newLabel("nuld.j"); B.condBr(isN, lu, lv); B.startBlock(lu); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lv); @@ -426,7 +426,7 @@ export function emitControlExpr( return host.own({ name: t, type: e.type }); } const l = host.emitExpr(e.left); - host.moveTemp(l); + host.toFrame(l); const ty = host.llType(e.type); const slot = B.slot(); B.entryAllocas.push(`${slot} = alloca ${ty}`); @@ -436,7 +436,7 @@ export function emitControlExpr( const lj = B.newLabel("nul.j"); B.condBr(isUnit, lu, lv); B.startBlock(lu); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); host.emitBranchInto(slot, e.right); B.br(lj); B.startBlock(lv); @@ -445,7 +445,7 @@ export function emitControlExpr( B.line(`store ${ty} ${l.name}, ptr ${slot}`); } else { B.line(`store ${ty} ${host.unionExtract(l.name, def.id, e.type)}, ptr ${slot}`); - host.releaseValue(l.name, e.left.type); + host.releaseFrame(l.name, e.left.type); } B.br(lj); B.startBlock(lj); @@ -533,7 +533,7 @@ export function emitOptionalChainReceiver( const isUnit = host.tagInSet(r.name, def.id, unitTags); const extract = (): string => multiple - ? host.retainValue(r.name, narrowed) + ? host.retainFrame(r.name, narrowed) : borrowBind ? host.unionPeek(r.name, def.id) : host.unionExtract(r.name, def.id, narrowed); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 76df9795d..8a24fcf23 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -161,7 +161,7 @@ export function emitDynamicExpr( ); host.emitPendingCheck(); } else { - host.moveTemp(v); + host.toHeap(v); B.line(`call void @scr_dyn_arr_push(ptr ${pack}, ptr ${v.name})`); } }); @@ -253,7 +253,7 @@ export function emitDynamicExpr( const out = host.own({ name: arr, type: e.type }); for (const el of e.elems) { const v = host.emitExpr(el); - host.moveTemp(v); + host.toHeap(v); B.line(`call void @scr_dyn_arr_push(ptr ${arr}, ptr ${v.name})`); } return out; @@ -296,7 +296,7 @@ export function emitDynamicExpr( return host.own({ name: host.unitInstanceRef(e.unionId, e.tag), type: e.type }); } const v = host.emitExpr(e.value); - if (isRefCounted(arm)) host.moveTemp(v); + if (isRefCounted(arm)) host.toHeap(v); return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type }); } case "unionNarrow": { @@ -353,7 +353,7 @@ export function emitDynamicExpr( const value = nullable ? host.nullableToOwnedUnion(v, nullable) : isRefCounted(e.type) - ? host.retainValue(v, e.type) + ? host.retainFrame(v, e.type) : v; B.line(`store ${ty} ${value}, ptr ${slot}`); B.br(join); @@ -465,7 +465,7 @@ export function emitDynamicExpr( const owned = inline ? host.inlineFieldUnion(ptr, ft) : isRefCounted(ft) - ? host.retainValue(v, ft) + ? host.retainFrame(v, ft) : v; B.line(`store ${ty} ${owned}, ptr ${slot}`); B.br(join); @@ -487,7 +487,7 @@ export function emitDynamicExpr( ft.kind === "f64" || ft.kind === "bool" ? host.unionNewOwned(e.type.unionId, tag, { name: v, type: ft }) : host.unionNewOwned(e.type.unionId, tag, { - name: host.retainValue(v, ft), + name: host.retainHeap(v, ft), type: ft, }); B.line(`store ptr ${wrapped}, ptr ${slot}`); @@ -1087,7 +1087,7 @@ export function emitDynamicExpr( B.line(`${pp} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`); B.line(`${payload} = load ptr, ptr ${pp}`); if (e.type.kind === "string") { - return host.own({ name: host.retainValue(payload, e.type), type: e.type }); + return host.own({ name: host.retainFrame(payload, e.type), type: e.type }); } if (e.type.kind === "object") { // Retain through the snapshot's own entry point (the payload's diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index e237f8f13..4e7d418f8 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -47,11 +47,11 @@ export function emitLiteralExpr( return { name: e.value ? "true" : "false", type: e.type }; case "strLit": { const sym = host.internLiteral(e.value); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } case "moduleNsRef": { const sym = host.internLiteral(`module:${e.moduleId}`); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } case "unitLit": // unitLits are consumed inline by the unionWrap case (a unit arm is @@ -110,10 +110,10 @@ export function emitLiteralExpr( const box = host.loadBox(b.slot); if (b.local!.tdz === true) { const v = host.tdzBoxRead(box, e.type, b.local!.name); - return host.own({ name: v, type: e.type }); + return host.own({ name: host.adoptHeap(v, e.type), type: e.type }); } const v = host.boxGet(box, e.type); - return host.own({ name: v, type: e.type }); + return host.own({ name: host.adoptHeap(v, e.type), type: e.type }); } const t = B.tmp(); B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`); @@ -131,7 +131,7 @@ export function emitLiteralExpr( return host.markImmortal({ name: t, type: e.type }); // The binding's last read takes over its reference (lastUseRead). if (host.takeLastUseMove(e, b.slot, b.type)) return host.own({ name: t, type: e.type }); - return host.own({ name: host.retainValue(t, e.type), type: e.type }); + return host.own({ name: host.retainFrame(t, e.type), type: e.type }); } return { name: t, type: e.type }; } @@ -488,8 +488,11 @@ export function emitOperatorExpr( if (isRefCounted(b.type)) { const old = B.tmp(); B.line(`${old} = load ptr, ptr ${b.slot}`); - host.releaseValue(old, b.type); - B.line(`store ptr ${host.retainValue(v.name, v.type)}, ptr ${b.slot}`); + if (b.kind === "global") host.releaseHeap(old, b.type); + else host.releaseFrame(old, b.type); + B.line( + `store ptr ${b.kind === "global" ? host.retainHeap(v.name, v.type) : host.retainFrame(v.name, v.type)}, ptr ${b.slot}`, + ); } else { B.line(`store ${host.llType(b.type)} ${v.name}, ptr ${b.slot}`); host.storeIntegerView(e.localId, v, e.value); @@ -632,12 +635,12 @@ export function emitStringExpr( case "undefinedT": case "nullT": { const lit = host.internLiteral(arm.kind === "undefinedT" ? "undefined" : "null"); - B.line(`store ptr ${host.retainValue(lit, e.type)}, ptr ${slot}`); + B.line(`store ptr ${host.retainFrame(lit, e.type)}, ptr ${slot}`); break; } case "string": B.line( - `store ptr ${host.retainValue(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`, + `store ptr ${host.retainFrame(host.unionPeek(v.name, def.id), e.type)}, ptr ${slot}`, ); break; case "f64": { @@ -691,7 +694,7 @@ export function emitStringExpr( // String(record) / `${record}`: Object.prototype.toString's // constant — the interned literal, retained like a strLit. const sym = host.internLiteral("[object Object]"); - return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } if (v.type.kind === "caught") { // String(e) / `${e}` on a catch binding: JS's String() over the @@ -748,7 +751,7 @@ export function emitStringExpr( }; host.templateStringsInstances.set(e.key, inst); } - return host.own({ name: host.retainValue(`@${inst.sym}`, e.type), type: e.type }); + return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type }); } case "regexIntrinsic": return host.emitRegexIntrinsic(e); @@ -975,7 +978,7 @@ export function emitRecordExpr( } const v = host.loadField(ptr, type, false); if (isRefCounted(e.type)) - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); return { name: v, type: e.type }; } case "recordGet": { @@ -984,7 +987,7 @@ export function emitRecordExpr( if (inline) return host.own({ name: host.inlineFieldUnion(ptr, type), type: e.type }); const v = host.loadRecordField(ptr, type, false); if (isRefCounted(e.type)) - return host.own({ name: host.retainValue(v, e.type), type: e.type }); + return host.own({ name: host.retainFrame(v, e.type), type: e.type }); return { name: v, type: e.type }; } case "recordLit": { @@ -1090,7 +1093,7 @@ export function emitRecordLitFields( if (f.overflow) { const lit = host.internLiteral(f.name); const acc = v.type.kind === "f64" ? "f64" : v.type.kind === "bool" ? "bool" : "ref"; - if (acc === "ref") host.moveTemp(v); + if (acc === "ref") host.toHeap(v); const ovf = host.recordOvfPtr(rec, shapeId); const argTy = acc === "f64" ? "double" : acc === "bool" ? "i1 zeroext" : "ptr"; host.declare(`declare void @scr_map_set_str_${acc}(ptr, ptr, ${argTy})`); diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index 0cbfe15dd..bca728e9d 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -41,7 +41,7 @@ export function emitUnionWiden( B.br(join); }); B.startBlock(join); - if (owned) host.releaseValue(value, { kind: "union", unionId: fromId }); + if (owned) host.releaseFrame(value, { kind: "union", unionId: fromId }); const result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); return result; @@ -58,7 +58,7 @@ export function emitUnionRetype( e: IrExpr & { kind: "unionRetype" }, ): LlValue { const value = host.emitExpr(e.value); - host.moveTemp(value); + host.toFrame(value); const from = host.nullableUnions.get(e.fromId); const to = host.nullableUnions.get(e.unionId); if (from && to) { @@ -131,7 +131,7 @@ function rebuildRetype( B.br(join); }); B.startBlock(join); - host.releaseValue(value, { kind: "union", unionId: e.fromId }); + host.releaseFrame(value, { kind: "union", unionId: e.fromId }); const result = B.tmp(); B.line(`${result} = load ptr, ptr ${slot}`); return result; @@ -486,10 +486,10 @@ export function keyedRecordReadInto( if (!typeEquals(vt, resultType)) { throw new LlvmUnsupportedError(`recordKeyGet:narrow:${vt.kind}`, loc); } - return owned || !isRefCounted(vt) ? expr : host.retainValue(expr, vt); + return owned || !isRefCounted(vt) ? expr : host.retainFrame(expr, vt); } if (typeEquals(vt, resultType)) { - return owned ? expr : host.retainValue(expr, vt); + return owned ? expr : host.retainFrame(expr, vt); } if (vt.kind === "union" && resultType.kind === "union") { return emitUnionWiden(host, expr, vt.unionId, resultType.unionId, owned); @@ -500,7 +500,7 @@ export function keyedRecordReadInto( if (vt.kind === "f64" || vt.kind === "bool") { return host.unionNewOwned(def.id, tag, { name: expr, type: vt }); } - const payload = owned ? expr : host.retainValue(expr, vt); + const payload = owned ? expr : host.retainHeap(expr, vt); return host.unionNewOwned(def.id, tag, { name: payload, type: vt }); }; for (const f of overflowOnly ? [] : shape.fields) { diff --git a/packages/compiler/src/backend/llvm/lib-abi.ts b/packages/compiler/src/backend/llvm/lib-abi.ts index 52d3201b7..6917d8256 100644 --- a/packages/compiler/src/backend/llvm/lib-abi.ts +++ b/packages/compiler/src/backend/llvm/lib-abi.ts @@ -85,7 +85,7 @@ export function callbackAdapter( symbol: string, params: string[], ): AbiArgument[] { - host.moveTemp(value); + host.toHeap(value); host.declare(`declare void @${symbol}(${params.join(", ")})`); return [ptr(value.name), ptr(`@${symbol}`)]; } diff --git a/packages/compiler/src/backend/llvm/lib-connect.ts b/packages/compiler/src/backend/llvm/lib-connect.ts index ba828fa48..ce9c40e21 100644 --- a/packages/compiler/src/backend/llvm/lib-connect.ts +++ b/packages/compiler/src/backend/llvm/lib-connect.ts @@ -47,7 +47,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L type = callbackType(cb), arity = type.params.length, second = type.params[1]; - host.moveTemp(cb); + host.toHeap(cb); let h1: string, h2 = "null"; if (second?.kind === "union") { diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts index 3fae845e2..b32446bcb 100644 --- a/packages/compiler/src/backend/llvm/lib-datagram.ts +++ b/packages/compiler/src/backend/llvm/lib-datagram.ts @@ -131,7 +131,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dgram.bindCb": case "dgram.connect": case "dgram.connectCb": - if (args[3] !== undefined) host.moveTemp(args[3]); + if (args[3] !== undefined) host.toHeap(args[3]); return finishRuntimeCall( host, e, @@ -140,7 +140,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L ); case "dgram.close": case "dgram.closeCb": - if (args[1] !== undefined) host.moveTemp(args[1]); + if (args[1] !== undefined) host.toHeap(args[1]); return finishRuntimeCall(host, e, "scr_dgram_close", [a[0]!, a[1] ?? ptr()]); case "dgram.address": { const type = e.type; @@ -151,7 +151,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L const record = host.B.tmp(); host.B.line(`${record} = call ptr @${mangleRecordNew(type.shapeId)}()`); const out = host.own({ name: record, type }); - host.moveTemp(ip); + host.toHeap(ip); const shape = host.recordsById.get(type.shapeId); if (shape === undefined) throw new InternalCompilerError("missing datagram address shape"); for (const field of ["address", "family", "port"]) { @@ -185,7 +185,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L ? "scr_dgram_msg_thunk1" : messageAdapter(host, type.params[1]!); // Program-specific adapters are definitions, so do not redeclare them. - host.moveTemp(cb); + host.toHeap(cb); if (type.params.length < 2) host.declare(`declare void @${adapter}(ptr, ptr, ptr, ptr, double, double)`); return finishRuntimeCall(host, e, "scr_dgram_on_message", [ @@ -213,7 +213,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dgram.onListening": case "dgram.onClose": case "dgram.onConnect": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall( host, e, @@ -227,7 +227,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L case "dns.lookup": { const cb = args[2]!, symbol = lookupAdapter(host, cb); - host.moveTemp(cb); + host.toHeap(cb); if (callbackType(cb).params.length === 0) host.declare("declare void @scr_dns_thunk0(ptr, ptr, ptr, double)"); return finishRuntimeCall(host, e, "scr_dns_lookup", [...a, ptr("@" + symbol)]); diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 034f0b6f1..daa8e08f3 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -92,7 +92,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func"); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = cbT.params.length === 0 ? "scr_rl_answer_thunk0" @@ -165,7 +165,7 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue const ev = host.emitExpr(evArg); const cb = host.emitExpr(e.args[2]!); const once = host.emitExpr(e.args[3]!); - host.moveTemp(cb); + host.toHeap(cb); let adapter = "null"; if (evArg.value === 0 || evArg.value === 4) { const param = cbT.params[0]; @@ -287,7 +287,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[4]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: randomFill callback not a func"); - host.moveTemp(args[4]!); + host.toHeap(args[4]!); const dyn = e.fn === "crypto.randomFillCbDyn"; const adapter = host.cryptoBytesThunkFor(cbT, false, dyn ? "dyn" : "view"); const symbol = dyn ? "scr_crypto_random_fill_async_dyn" : "scr_crypto_random_fill_async"; @@ -305,7 +305,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: generateKey callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.cryptoBytesThunkFor(cbT, false, "dyn"); host.declare(`declare void @scr_crypto_generate_key_async(ptr, ptr, ptr, ptr)`); B.line( @@ -320,7 +320,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[3]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: randomInt callback not a func"); - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const adapter = host.cryptoIntThunkFor(cbT); host.declare(`declare void @scr_crypto_random_int_async(double, double, double, ptr, ptr)`); B.line( @@ -429,7 +429,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[cbIndex]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: crypto callback not a func"); - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); const adapter = host.cryptoBytesThunkFor(cbT, e.fn === "crypto.hkdfCb"); if (e.fn === "crypto.randomBytesCb") { host.declare(`declare void @scr_crypto_random_bytes_async(double, ptr, ptr)`); @@ -486,7 +486,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[cbIdx]!.type; if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); - host.moveTemp(args[cbIdx]!); + host.toHeap(args[cbIdx]!); cb = args[cbIdx]!.name; const sym = cbT.params.length === 0 ? "scr_http_resp_thunk0" : "scr_http_resp_thunk_res"; host.declare(`declare void @${sym}(ptr, ptr)`); @@ -722,7 +722,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { // Literals (`123n`) and constant BigInt("...") texts are immortal // statics instead of a parse at every evaluation. const sym = host.internBigIntLiteral(e.args[0].value); - if (sym) return host.own({ name: host.retainValue(sym, e.type), type: e.type }); + if (sym) return host.own({ name: host.retainFrame(sym, e.type), type: e.type }); } if (e.fn.startsWith("bigint.")) { // Small-value operator trees run in 64-bit registers (bigint-wrap.ts). diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index 74278789c..4798b1f09 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -193,7 +193,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[1]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: zlib callback not a func"); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = host.zlibBytesThunkFor(cbT); const compressing = e.fn === "zlib.deflateCb" || e.fn === "zlib.deflateRawCb" || e.fn === "zlib.gzipCb"; @@ -246,7 +246,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[3]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: zlib callback not a func"); - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const adapter = host.zlibBytesThunkFor(cbT); const sym = e.fn === "zlib.cbOpts" ? "scr_zlib_codec_async_opts" : "scr_zlib_codec_async_opts_dyn"; @@ -293,7 +293,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const operands = args.slice(0, withCb ? -1 : undefined).map((a) => `ptr ${a.name}`); let cb = "null"; if (withCb) { - host.moveTemp(args[args.length - 1]!); + host.toHeap(args[args.length - 1]!); cb = args[args.length - 1]!.name; } const sym = @@ -356,7 +356,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: fs.rename callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.fsRenameThunkFor(cbT); host.declare(`declare void @scr_fs_rename_async(ptr, ptr, ptr, ptr)`); B.line( @@ -403,7 +403,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): const countField = e.fn === "fileHandle.read" || e.fn === "fileHandle.readv" ? "bytesRead" : "bytesWritten"; host.storeField(host.recordFieldPtr(rec, inner.shapeId, countField).ptr, F64, count, false); - const payload = host.retainValue(args[1]!.name, e.args[1]!.type); + const payload = host.retainHeap(args[1]!.name, e.args[1]!.type); B.line( `store ptr ${payload}, ptr ${host.recordFieldPtr(rec, inner.shapeId, vector ? "buffers" : "buffer").ptr}`, ); @@ -465,7 +465,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): throw new InternalCompilerError("llvm emitter bug: fs.watchCb callback not a func"); const path = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`); @@ -618,7 +618,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): B.line(`${nm} = call ptr @scr_fs_scandir_name(ptr ${snap}, ${host.sizeType} ${i}) ; +1`); B.line(`store ptr ${nm}, ptr ${host.recordFieldPtr(row, recT.shapeId, "name").ptr}`); B.line( - `store ptr ${host.retainValue(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, + `store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`, ); host.arrPush(arr, "ref", row); // push takes ownership of the row const i2 = B.tmp(); @@ -630,7 +630,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): if (!promiseForm) return arrayOut; const rc = vAdapters(host.shapeHost, arrayT); host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`); - host.moveTemp(arrayOut); // promise fulfillment owns the result array + host.toHeap(arrayOut); // promise fulfillment owns the result array const promise = B.tmp(); B.line( `${promise} = call ptr @scr_promise_settled_ref(ptr ${arr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, arrayT)})`, @@ -696,7 +696,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): if (!promiseForm) return value; const rc = vAdapters(host.shapeHost, unionT); host.declare(`declare ptr @scr_promise_settled_ref(ptr, ptr, ptr, ptr)`); - host.moveTemp(value); + host.toHeap(value); const promise = B.tmp(); B.line( `${promise} = call ptr @scr_promise_settled_ref(ptr ${value.name}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, unionT)})`, @@ -941,7 +941,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const retained = B.tmp(); B.line(`${retained} = call ptr @scr_arr_retain_v(ptr ${peeked})`); B.line(`store ptr ${retained}, ptr ${rowsSlot}`); - host.releaseValue(cell, iv); + host.releaseFrame(cell, iv); B.br(lj); B.startBlock(lm); const fresh = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-http2.ts b/packages/compiler/src/backend/llvm/lib-http2.ts index f4c57c63e..0254db555 100644 --- a/packages/compiler/src/backend/llvm/lib-http2.ts +++ b/packages/compiler/src/backend/llvm/lib-http2.ts @@ -170,7 +170,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa if (tag < 0) throw new InternalCompilerError("SNI callback union lacks its function arm"); type = arms[tag]!; value = host.unwrapNullableClosure(value, tag); - } else host.moveTemp(cb); + } else host.toHeap(cb); if (type.kind !== "func" || type.params[1] === undefined) throw new InternalCompilerError("SNI callback lacks its answer function"); sni = [ptr(value), ptr("@" + sniAnswer(host, type.params[1]))]; @@ -207,7 +207,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa case "http2.streamCloseCb": { const stream = e.fn.startsWith("http2.stream"), cbIndex = stream ? 2 : 1; - if (args[cbIndex] !== undefined) host.moveTemp(args[cbIndex]!); + if (args[cbIndex] !== undefined) host.toHeap(args[cbIndex]!); return finishRuntimeCall( host, e, @@ -310,7 +310,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa default: throw new InternalCompilerError(`unhandled HTTP/2 listener ${e.fn}`); } - if (thunk === null) host.moveTemp(cb); + if (thunk === null) host.toHeap(cb); const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params); return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]); } diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts index a6e76e4e0..c6bc4d97b 100644 --- a/packages/compiler/src/backend/llvm/lib-lookup.ts +++ b/packages/compiler/src/backend/llvm/lib-lookup.ts @@ -86,7 +86,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV ); } host.usesTimers = true; - host.moveTemp(cb); + host.toHeap(cb); return finishRuntimeCall(host, e, "scr_net_connect_lookup", [ ...args.map((arg) => abiValue(host, arg)), ptr("@" + symbol), diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts index 322479a85..ea37fc2cf 100644 --- a/packages/compiler/src/backend/llvm/lib-network.ts +++ b/packages/compiler/src/backend/llvm/lib-network.ts @@ -42,7 +42,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (e.fn === "http.requestConn" || e.fn === "http.requestConnCb") { const args = e.args.map((arg) => host.emitExpr(arg)); host.usesTimers = true; - host.moveTemp(args[0]!); + host.toHeap(args[0]!); const cb = args[6]; const handler = cb === undefined @@ -72,7 +72,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const cbT = e.args[0]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: net.createServerCb handler not a func"); - host.moveTemp(args[0]!); + host.toHeap(args[0]!); cb = args[0]!.name; adapter = cbT.params.length === 0 ? "@scr_net_conn_thunk0" : "@scr_net_conn_thunk_sock"; host.declare(`declare void ${adapter}(ptr, ptr)`); @@ -86,7 +86,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.listenCb") { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare void @scr_net_listen(ptr, double, ptr)`); @@ -109,7 +109,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (withCallback) { const cbT = e.args[cbIndex]!.type; if (cbT.kind === "func") { - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); cb = args[cbIndex]!.name; } else { if (cbT.kind !== "union") @@ -192,7 +192,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.serverCloseCb") { - host.moveTemp(args[1]!); + host.toHeap(args[1]!); cb = args[1]!.name; } host.declare(`declare void @scr_net_server_close(ptr, ptr)`); @@ -233,7 +233,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) throw new InternalCompilerError("llvm emitter bug: close override not a func"); const args = e.args.map((a) => host.emitExpr(a)); const wrapSym = host.closeOverrideWrapFor(cbT.params[0]!, cbT.ret.kind === "netServer"); - host.moveTemp(args[1]!); // ownership moves into the wrapper's env box + host.toHeap(args[1]!); // ownership moves into the wrapper's env box const wrap = host.wrapEmitterListener(args[1]!.name, wrapSym); host.declare(`declare void @scr_net_server_set_close_override(ptr, ptr)`); B.line(`call void @scr_net_server_set_close_override(ptr ${args[0]!.name}, ptr ${wrap})`); @@ -244,7 +244,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; const entry = e.fn === "net.serverOnError" ? "scr_net_server_on_error" : "scr_net_sock_on_error"; @@ -295,7 +295,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) "http.clientOnContinue": "scr_http_client_on_continue", }[e.fn]!; const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @${entry}(ptr, ptr, i1 zeroext)`); B.line(`call void @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, i1 ${args[2]!.name})`); return { name: "", type: e.type }; @@ -312,7 +312,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[cbIndex]!); + host.toHeap(args[cbIndex]!); const adapter = cbT.params.length === 0 ? "scr_net_conn_thunk0" : "scr_net_conn_thunk_sock"; host.declare(`declare void @${adapter}(ptr, ptr)`); if (e.fn === "http.serverSetTimeoutCb") { @@ -340,7 +340,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn === "net.connectCb") { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare ptr @scr_net_connect_checked(double, ptr, ptr)`); @@ -357,7 +357,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 0 ? "scr_net_data_thunk0" @@ -533,7 +533,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) const cbT = e.args[0]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: http.createServer handler not a func"); - host.moveTemp(args[0]!); + host.toHeap(args[0]!); cb = args[0]!.name; const sym = cbT.params.length === 2 @@ -554,7 +554,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} handler not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 2 ? "scr_http_handler_thunk2" @@ -580,7 +580,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = cbT.params.length === 3 ? "scr_http_upgrade_thunk3" @@ -607,7 +607,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const adapter = e.fn === "http.clientOnResponse" ? cbT.params.length === 0 @@ -630,7 +630,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.resOnFinish" || e.fn === "http.resOnEndCallback") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); const entry = e.fn === "http.resOnFinish" ? "scr_http_res_on_finish" : "scr_http_res_on_end_callback"; host.declare(`declare void @${entry}(ptr, ptr)`); @@ -639,7 +639,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.resSetTimeoutCb" || e.fn === "http.reqSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const entry = e.fn === "http.resSetTimeoutCb" ? "scr_http_res_set_timeout" : "scr_http_req_set_timeout"; host.declare(`declare void @${entry}(ptr, double, ptr)`); @@ -650,7 +650,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "http.clientSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare("declare void @scr_http_client_set_timeout_cb(ptr, double, ptr)"); B.line( `call void @scr_http_client_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, @@ -659,21 +659,21 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr) } if (e.fn === "net.sockOnFinish") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @scr_net_sock_on_finish(ptr, ptr)`); B.line(`call void @scr_net_sock_on_finish(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } if (e.fn === "net.sockOnWriteFlush") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[1]!); + host.toHeap(args[1]!); host.declare(`declare void @scr_net_sock_on_write_flush(ptr, ptr)`); B.line(`call void @scr_net_sock_on_write_flush(ptr ${args[0]!.name}, ptr ${args[1]!.name})`); return { name: "", type: e.type }; } if (e.fn === "net.sockSetTimeoutCb") { const args = e.args.map((a) => host.emitExpr(a)); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare("declare void @scr_net_sock_set_timeout_cb(ptr, double, ptr)"); B.line( `call void @scr_net_sock_set_timeout_cb(ptr ${args[0]!.name}, double ${args[1]!.name}, ptr ${args[2]!.name})`, diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 3c35ae6dc..e6d8acdbe 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -18,7 +18,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr const cmd = host.emitExpr(e.args[0]!); const argv = host.emitExpr(e.args[1]!); const cb = host.emitExpr(callbackArg); - host.moveTemp(cb); + host.toHeap(cb); const shell = host.emitExpr(e.args[3]!); const adapter = host.execFileThunkFor(cbT); host.usesTimers = true; @@ -72,7 +72,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (e.fn === "child.onSpawn") { const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare("declare void @scr_child_on_spawn(ptr, ptr)"); B.line(`call void @scr_child_on_spawn(ptr ${child.name}, ptr ${cb.name})`); return { name: "", type: e.type }; @@ -87,7 +87,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); let adapter: string; if (cbT.params.length === 0) { adapter = "scr_child_exit_thunk0"; @@ -110,7 +110,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError("llvm emitter bug: child.onError callback not a func"); const child = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_child_err_thunk0" : "scr_child_err_thunk_error"; host.declare(`declare void @${adapter}(ptr, ptr)`); host.declare(`declare void @scr_child_on_error(ptr, ptr, ptr)`); @@ -136,7 +136,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: child.send callback not a func"); const cb = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`; decl += ", ptr, ptr"; } @@ -160,7 +160,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "child.onMessage") { host.declare(`declare void @scr_child_ipc_on_message(ptr, ptr, ptr, i1 zeroext)`); B.line( @@ -337,7 +337,7 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr if (cbT.kind !== "func") throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const once = host.emitExpr(e.args[2]!); if (e.fn === "writer.onError") { const adapter = @@ -373,7 +373,7 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr // main runs the loop. host.usesTimers = true; const cb = host.emitExpr(e.args[0]!); - host.moveTemp(cb); + host.toHeap(cb); // Programs that enter AsyncLocalStorage stores schedule through the // *_als entry points, which carry the active stores into the callback. const sym = @@ -466,7 +466,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: process.send callback not a func"); const cb = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); suffix = `, ptr ${cb.name}, ptr @${host.ipcSendThunkFor(cbT)}`; decl += ", ptr, ptr"; } @@ -488,7 +488,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "process.onMessage") { host.declare(`declare void @scr_process_ipc_on_message(ptr, ptr, i1 zeroext)`); B.line( @@ -509,7 +509,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const cbT = e.args[2]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: process write callback not a func"); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); const adapter = host.fsRenameThunkFor(cbT); const write = e.fn === "process.stdoutWriteBytesCb" @@ -540,7 +540,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); if (e.fn === "stdin.onEnd") { host.declare(`declare void @scr_stdin_on_end(ptr, i1 zeroext)`); B.line(`call void @scr_stdin_on_end(ptr ${cb.name}, i1 ${once.name})`); @@ -588,7 +588,7 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll throw new InternalCompilerError("llvm emitter bug: process.onExit callback not a func"); const cb = host.emitExpr(e.args[0]!); const once = host.emitExpr(e.args[1]!); - host.moveTemp(cb); + host.toHeap(cb); const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code"; host.declare(`declare void @${adapter}(ptr, double)`); host.declare(`declare void @scr_process_on_exit(ptr, ptr, i1 zeroext)`); @@ -743,7 +743,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const cbr = B.tmp(); B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`); const wrapped = host.wrapEmitterListener(cbr, adapterFn); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare( `declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, ); @@ -767,7 +767,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func"); const args = e.args.map((a) => host.emitExpr(a)); const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT); - host.moveTemp(args[3]!); // the frame's +1 moves into the wrapper's box + host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn); host.declare( `declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, @@ -866,7 +866,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`); const t = B.tmp(); if (isDyn) { - host.moveTemp(args[3]!); + host.toHeap(args[3]!); const wrapped = host.wrapEmitterListener(args[3]!.name, adapterFn); host.declare( `declare ptr @scr_emitter_on_dyn(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, @@ -880,7 +880,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr const cbr = B.tmp(); B.line(`${cbr} = call ptr @scr_closure_retain_v(ptr ${args[2]!.name})`); const wrapped = host.wrapEmitterListener(cbr, adapterFn); - host.moveTemp(args[2]!); + host.toHeap(args[2]!); host.declare( `declare ptr @scr_emitter_on_via(ptr, ptr, ptr, ptr, ptr, i1 zeroext, i1 zeroext)`, ); diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index dbb6d0181..b62b237df 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -48,7 +48,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); const param = cbT.params[0]; let adapter: string; if (param === undefined) { @@ -70,7 +70,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare(`declare void @scr_child_stream_on_end(ptr, ptr, i1 zeroext)`); B.line(`call void @scr_child_stream_on_end(ptr ${s0.name}, ptr ${cb.name}, i1 ${once.name})`); return { name: "", type: e.type }; @@ -86,7 +86,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const s0 = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const once = host.emitExpr(e.args[2]!); - host.moveTemp(cb); + host.toHeap(cb); host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`); host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`); B.line( @@ -127,7 +127,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } const cb = args[at]!; const cbT = e.args[at]!.type; - host.moveTemp(cb); // the callback closure MOVES into the stream + host.toHeap(cb); // the callback closure MOVES into the stream cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`); at++; } @@ -182,7 +182,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const recv = host.emitExpr(e.args[0]!); const cb = host.emitExpr(e.args[1]!); const cbT = e.args[1]!.type; - host.moveTemp(cb); // the callback closure MOVES into the stream + host.toHeap(cb); // the callback closure MOVES into the stream const sym = symOf[e.fn]!; host.declare(`declare void @${sym}(ptr, ptr, ptr)`); B.line( @@ -210,7 +210,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV } const cb = args[at]!; const cbT = e.args[at]!.type; - host.moveTemp(cb); + host.toHeap(cb); cbArgs.push(`ptr ${cb.name}`, `ptr @${host.streamCbThunkFor(canonical[i]!, cbT)}`); at++; } @@ -321,7 +321,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV B.line(`${raw} = call ptr @scr_stream_read(ptr ${args[0]!.name}, double ${args[1]!.name})`); const b = host.own({ name: raw, type: def!.arms[bytesTag]! }); host.emitPendingCheck(); - host.moveTemp(b); // moves into the union arm when present + host.toHeap(b); // moves into the union arm when present return host.wrapNullable(raw, raw, def!.arms[bytesTag]!, bytesTag, e.type, nullTag); } if (e.fn === "readable.flowing") { @@ -383,7 +383,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const args = e.args.map((a) => host.emitExpr(a)); let cb = "null"; if (e.fn !== "writable.writeDyn" && e.args.length > 2) { - host.moveTemp(args[2]!); + host.toHeap(args[2]!); cb = args[2]!.name; } host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`); @@ -412,7 +412,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV else if (flags & 8) chunkD = args[at++]!.name; let cbName = "null"; if (flags & 4) { - host.moveTemp(args[at]!); + host.toHeap(args[at]!); cbName = args[at]!.name; } if (chunkD !== null) { @@ -490,7 +490,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const cbT = e.args[1]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream.finished callback not a func"); - host.moveTemp(args[1]!); // the watcher closure MOVES into the stream + host.toHeap(args[1]!); // the watcher closure MOVES into the stream const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_finished(ptr, ptr, ptr)`); B.line( @@ -599,7 +599,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV throw new InternalCompilerError( "llvm emitter bug: stream.finishedOpts callback not a func", ); - host.moveTemp(args[cbIdx]!); + host.toHeap(args[cbIdx]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare( `declare ptr @scr_stream_finished_opts(ptr, ptr, ptr, double, double, i1, ptr, ptr)`, @@ -696,7 +696,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV throw new InternalCompilerError( "llvm emitter bug: stream.pipelineStages callback not a func", ); - host.moveTemp(args[args.length - 1]!); + host.toHeap(args[args.length - 1]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_pipeline_stages(${params}, ptr, ptr)`); B.line( @@ -732,7 +732,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const cbT = e.args[1 + n]!.type; if (cbT.kind !== "func") throw new InternalCompilerError("llvm emitter bug: stream.pipeline callback not a func"); - host.moveTemp(args[1 + n]!); + host.toHeap(args[1 + n]!); const thunk = host.streamCbThunkFor("e", cbT); host.declare(`declare ptr @scr_stream_pipeline(double, ptr, ptr, ptr)`); B.line( diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 37e642d88..4780311f8 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -8,7 +8,7 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal host.usesTimers = true; switch (e.fn) { case "test.register": - host.moveTemp(args[3]!); + host.toHeap(args[3]!); return finishRuntimeCall(host, e, "scr_test_register", a); case "test.registerEmpty": return finishRuntimeCall(host, e, "scr_test_register", [ @@ -17,13 +17,13 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal ...a.slice(3), ]); case "test.suite": - host.moveTemp(args[3]!); + host.toHeap(args[3]!); return finishRuntimeCall(host, e, "scr_test_suite", a); case "test.hook": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_test_hook", a); case "test.sub": - host.moveTemp(args[4]!); + host.toHeap(args[4]!); return finishRuntimeCall(host, e, "scr_test_sub", a); case "test.subEmpty": { const promise = callRuntime(host, "scr_test_sub", "ptr", [ @@ -50,21 +50,21 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal case "test.workerId": return finishRuntimeCall(host, e, "scr_test_worker_id", a); case "test.ctxHook": - host.moveTemp(args[2]!); + host.toHeap(args[2]!); return finishRuntimeCall(host, e, "scr_test_ctx_hook", a); case "test.ctxPlan": return finishRuntimeCall(host, e, "scr_test_ctx_plan", a); case "test.ctxCount": return finishRuntimeCall(host, e, "scr_test_ctx_count", a); case "test.mockTrack": - host.moveTemp(args[0]!); + host.toHeap(args[0]!); return finishRuntimeCall(host, e, "scr_test_mock_track", a); case "test.mockRestoreAll": return finishRuntimeCall(host, e, "scr_test_mock_restore_all", a); case "test.mockReset": return finishRuntimeCall(host, e, "scr_test_mock_reset", a); case "test.ctxMockTrack": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_test_ctx_mock_track", a); case "test.ctxMockRestoreAll": return finishRuntimeCall(host, e, "scr_test_ctx_mock_restore_all", a); diff --git a/packages/compiler/src/backend/llvm/lib-tls.ts b/packages/compiler/src/backend/llvm/lib-tls.ts index a993ec49a..18fc006d4 100644 --- a/packages/compiler/src/backend/llvm/lib-tls.ts +++ b/packages/compiler/src/backend/llvm/lib-tls.ts @@ -94,10 +94,10 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu } case "tls.connect": case "tls.connectCb": - if (args[3] !== undefined) host.moveTemp(args[3]); + if (args[3] !== undefined) host.toHeap(args[3]); return finishRuntimeCall(host, e, "scr_tls_connect_dyn", [...a.slice(0, 3), a[3] ?? ptr()]); case "tls.sockOnSecureConnect": - host.moveTemp(args[1]!); + host.toHeap(args[1]!); return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a); case "tls.sockOnSession": { const cb = args[1]!, diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts index b31c02ad0..6951896d2 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.ts @@ -629,7 +629,7 @@ export function emitLocalArrayRead( // A slot of a nullable-union array may hold NULL even when present. const owned = read.nullableElement ? read.type : read.element; const store = (value: string): void => { - B.line(`store ptr ${read.borrow ? value : host.retainValue(value, owned)}, ptr ${localSlot}`); + B.line(`store ptr ${read.borrow ? value : host.retainFrame(value, owned)}, ptr ${localSlot}`); }; if (inline) emitDenseReferenceArrayRead(host, array, index, integerIndex, store, no, slow, join, "ptr"); @@ -676,7 +676,7 @@ export function emitLocalArrayRead( B.line(`store i64 ${bits}, ptr ${payload}`); } else { B.line( - `store ptr ${read.borrow ? value : host.retainValue(value, read.element)}, ptr ${payload}`, + `store ptr ${read.borrow ? value : host.retainFrame(value, read.element)}, ptr ${payload}`, ); } B.line(`store i32 ${read.presentTag}, ptr ${tag}`); diff --git a/packages/compiler/src/backend/llvm/local-union-storage.ts b/packages/compiler/src/backend/llvm/local-union-storage.ts index af65920dc..445ff45de 100644 --- a/packages/compiler/src/backend/llvm/local-union-storage.ts +++ b/packages/compiler/src/backend/llvm/local-union-storage.ts @@ -202,7 +202,7 @@ export function storeLocalUnion( B.startBlock(label); host.frames.push([]); storeLocalUnion(host, storage, value); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(join); } B.startBlock(join); @@ -241,7 +241,7 @@ export function storeLocalUnion( const copied = host.emitOwnedNullableStack(expr); source = copied.box; owner = copied.owner.name; - host.moveTemp(copied.owner); + host.toFrame(copied.owner); } else if (expr.kind === "varRef" && !canStackUnion(expr, host.boxedUnionsById)) { // A copy of another binding: snapshot the tag and bits, and retain the // reference payload (scalar and unit arms own nothing). @@ -260,7 +260,7 @@ export function storeLocalUnion( B.line(`${isRef} = icmp eq i32 ${tag}, ${refTag}`); B.condBr(isRef, yes, join); B.startBlock(yes); - const retained = host.retainValue(host.unionPeek(source, unionId), storage.ownerType); + const retained = host.retainFrame(host.unionPeek(source, unionId), storage.ownerType); B.line(`store ptr ${retained}, ptr ${held}`); B.br(join); B.startBlock(join); @@ -272,7 +272,7 @@ export function storeLocalUnion( source = wrapped.value.name; if (wrapped.payload) { owner = wrapped.payload.name; - host.moveTemp(wrapped.payload); + host.toFrame(wrapped.payload); } } else { const read = matchMapRead(expr, host.boxedUnionsById)!; @@ -294,7 +294,7 @@ export function storeLocalUnion( const old = B.tmp(); B.line(`${old} = load ptr, ptr ${storage.owner}`); B.line(`store ptr ${owner}, ptr ${storage.owner}`); - host.releaseValue(old, storage.ownerType); + host.releaseFrame(old, storage.ownerType); } } @@ -326,7 +326,7 @@ export function emitScalarOptional( bindPresent(); host.frames.push([]); storeLocalUnion(host, storage, expr.body); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); host.chainSlots.delete(expr.id); B.br(join); } else { @@ -339,7 +339,7 @@ export function emitScalarOptional( B.startBlock(fallback); host.frames.push([]); storeLocalUnion(host, storage, expr.right); - host.releaseFrame(host.frames.pop()!); + host.releaseTemps(host.frames.pop()!); B.br(join); } B.startBlock(join); diff --git a/packages/compiler/src/backend/llvm/map-number-lookup.ts b/packages/compiler/src/backend/llvm/map-number-lookup.ts index 57c517f6a..f0aa03497 100644 --- a/packages/compiler/src/backend/llvm/map-number-lookup.ts +++ b/packages/compiler/src/backend/llvm/map-number-lookup.ts @@ -105,7 +105,7 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy join = B.newLabel("mapn.join"); B.condBr(found, hit, miss); B.startBlock(hit); - const retained = host.retainValue(emitNumberMapSlot(host, entry, "ptr"), value); + const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value); B.br(join); B.startBlock(miss); B.br(join); @@ -251,7 +251,7 @@ export function emitMapIterSlot( B.line(`${loaded} = icmp ne i64 ${bits}, 0`); } else { B.line(`${loaded} = load ${llTy}, ptr ${slot}`); - if (access === "ref") loaded = host.retainValue(loaded, type); + if (access === "ref") loaded = host.retainFrame(loaded, type); } const hitEnd = B.currentLabel(); B.br(join); diff --git a/packages/compiler/src/backend/llvm/split-loops.ts b/packages/compiler/src/backend/llvm/split-loops.ts index b7f1d0ef9..c29705919 100644 --- a/packages/compiler/src/backend/llvm/split-loops.ts +++ b/packages/compiler/src/backend/llvm/split-loops.ts @@ -102,8 +102,8 @@ export function storeSplitSnapshot( host.B.line(`store ptr ${snapshot.source}, ptr ${sourceSlot}`); host.B.line(`store ptr ${snapshot.separator}, ptr ${separatorSlot}`); host.B.line(`store i32 ${snapshot.limit}, ptr ${limitSlot}`); - host.moveTemp({ name: snapshot.source, type: STRING }); - host.moveTemp({ name: snapshot.separator, type: STRING }); + host.toFrame({ name: snapshot.source, type: STRING }); + host.toFrame({ name: snapshot.separator, type: STRING }); return { sourceSlot, separatorSlot, limitSlot }; } diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index cbbf0cc29..c7a58a7ea 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -236,7 +236,7 @@ function emitRecordCloneFields( const fieldTy = llFieldType(from.type, from.inline); const v = B.tmp(); B.line(`${v} = load ${fieldTy}, ptr ${from.ptr}`); - const stored = isRefCounted(from.type) && !from.inline ? host.retainValue(v, from.type) : v; + const stored = isRefCounted(from.type) && !from.inline ? host.retainFrame(v, from.type) : v; B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`); } const initialized = new Set(); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index ae73f1ea1..d00c681d6 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -91,7 +91,7 @@ export function emitStringSliceSnapshot( B.line( `call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`, ); - host.moveTemp(source); + host.toFrame(source); return { source: sourceSlot, range }; } diff --git a/packages/compiler/src/backend/llvm/thread-slots.ts b/packages/compiler/src/backend/llvm/thread-slots.ts index da182bde2..85656878a 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.ts @@ -49,7 +49,7 @@ export function emitThreadSlotsIntrinsic( case "slots.get": { const read = readSlot(host, block, word); if (host.nullableUnions.of(e.type) !== null) - return host.own({ name: host.retainValue(read, e.type), type: e.type }); + return host.own({ name: host.retainFrame(read, e.type), type: e.type }); return boxedRead(host, read, e.type); } case "slots.has": { @@ -67,13 +67,13 @@ export function emitThreadSlotsIntrinsic( case "slots.set": { if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value"); // The value moves into the block. - host.moveTemp(value); + host.toHeap(value); host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`); B.line( `call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`, ); if (e.type.kind === "void") return { name: "", type: e.type }; - return host.own({ name: host.retainValue(slots.name, e.type), type: e.type }); + return host.own({ name: host.retainFrame(slots.name, e.type), type: e.type }); } default: throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`); @@ -100,7 +100,7 @@ function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValu B.condBr(has, some, none); B.startBlock(some); const arm = def.arms[refTag]!; - const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm }); + const box = host.unionNewOwned(def.id, refTag, { name: host.retainHeap(read, arm), type: arm }); B.line(`store ptr ${box}, ptr ${result}`); B.br(done); B.startBlock(none); From 309fa897ec6bec258a6a7712cb01e9583a1d2c52 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 04:48:10 -0500 Subject: [PATCH 65/67] Use EventEmitter, the stream classes and the error classes as values `function make(C: typeof EventEmitter) { return new C(); }`, `const kinds: ErrorConstructor[] = [TypeError, RangeError]` and `(typeof Readable)[]` stopped at SC2020 (the type has no mapping) or SC1090 (builtin classes as values). These classes are now class values: the name in a TypeScript file (or a const alias of it) is the class object, `typeof EventEmitter`, `typeof Readable` and ErrorConstructor- style types map to class values, and parameters, arrays, Map values and object fields hold them. Construction through a value calls %C.construct, which builds the instance with the runtime constructor the direct `new` uses (emitter.new, a stream's newDyn without options, error.newOptions for Error and its six standard subclasses). instanceof, typeof, .name and === answer as in Node. A Duplex value widens into a `typeof Readable` slot, and stream values widen into `typeof EventEmitter` slots, as the constructor ABIs match; a union of such class values (`const kinds = [Readable, Writable]`) is a value of their nearest common ancestor, and `new K()` through it answers the checker's union of instances. `instanceof` through a value that may be Writable keeps the Duplex rule at run time. EventEmitter and stream values constructed with options, runtime class values stored as untyped values or printed, statics read through a value, and DOMException, AggregateError, AssertionError and SuppressedError values report SC1090. The existing static lowerings (`EventEmitter.setMaxListeners`, `Error.stackTraceLimit`, the events module) are unchanged. JavaScript files keep the identity token. --- packages/compiler/src/backend/llvm/classes.ts | 26 ++- packages/compiler/src/backend/llvm/emitter.ts | 3 +- .../compiler/src/backend/llvm/expr-calls.ts | 6 +- .../lowering/builtins/class-values.ts | 55 ++++++ .../src/frontend/lowering/lower-calls.ts | 9 + .../src/frontend/lowering/lower-classes.ts | 174 ++++++++++++++++-- .../src/frontend/lowering/lower-exprs.ts | 172 +++++++++++++++++ .../src/frontend/lowering/lower-stmts.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 32 +++- packages/compiler/src/frontend/type-mapper.ts | 9 + packages/compiler/src/ir/analysis.ts | 3 +- packages/compiler/src/ir/ir.ts | 21 +++ packages/compiler/src/ir/validate.ts | 38 ++-- .../corpus/5981-runtime-class-values.ts.json | 6 + .../main.ts.json | 7 + .../runtime-class-value-fences.ts.json | 6 + tests/corpus/5981-runtime-class-values.ts | 117 ++++++++++++ .../5982-runtime-class-value-modules/kinds.ts | 14 ++ .../5982-runtime-class-value-modules/main.ts | 16 ++ .../diagnostics/builtin-class-alias-escape.ts | 14 +- tests/diagnostics/class-values.ts | 6 +- .../diagnostics/runtime-class-value-fences.ts | 23 +++ .../builtin-class-alias-escape.ts.txt | 26 +-- .../harness/__snapshots__/class-values.ts.txt | 8 +- .../runtime-class-value-fences.ts.txt | 63 +++++++ tests/harness/corpus-costs.json | 4 + 26 files changed, 786 insertions(+), 76 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json create mode 100644 tests/corpus/5981-runtime-class-values.ts create mode 100644 tests/corpus/5982-runtime-class-value-modules/kinds.ts create mode 100644 tests/corpus/5982-runtime-class-value-modules/main.ts create mode 100644 tests/diagnostics/runtime-class-value-fences.ts create mode 100644 tests/harness/__snapshots__/runtime-class-value-fences.ts.txt diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 02036e4d9..717b08d3b 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -11,6 +11,7 @@ import { RUNTIME_EVENT_TARGET_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, + classValueConstructor, } from "../../ir/ir.js"; import { streamRooted, undefinedArmTag } from "../../ir/analysis.js"; import { @@ -926,14 +927,32 @@ export function emitClassObjDefs( `llvm emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`, ); } - const ctor = fnByName.get(`%${className}.constructor`); + const valueCtor = classValueConstructor(meta.def); + const ctor = fnByName.get(valueCtor.fn); if (!ctor) throw new InternalCompilerError( `llvm emitter bug: class object for ${className} without a constructor`, ); - const params = ctor.params.slice(1); + const params = ctor.params.slice(valueCtor.firstArg); const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", "); const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`); + const classObj = + `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}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 ?? (meta.def.runtime ? 1 : 0)}, ptr null, ptr null } ; class ${className}`; + if (meta.def.runtime) { + // A runtime-provided class: %C.construct allocates through the + // runtime and returns the instance. + out.push( + `define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`, + `entry:`, + ` %o = call ptr @${mangleFunction(valueCtor.fn)}(${ctorArgs.join(", ")})`, + ` ret ptr %o`, + `}`, + classObj, + ``, + ); + continue; + } if (meta.def.localCaptures !== undefined) host.declare(`declare ptr @scr_classobj_retain_v(ptr)`); const localBaseInits: string[] = []; @@ -992,8 +1011,7 @@ export function emitClassObjDefs( ` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`, ` ret ptr %o`, `}`, - `@${mangleClassObj(className)} = internal ${threadLocal ? "thread_local " : ""}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, ptr null } ; class ${className}`, + classObj, ``, ); } diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 7e7e70df6..494b7a6c4 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -56,6 +56,7 @@ import { typeKey, STRING, VOID, + classJsName, } from "../../ir/ir.js"; import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js"; import { captureFreeFunctions, observesStacks } from "../stack-observations.js"; @@ -6123,7 +6124,7 @@ export class LlEmitter { classObjSym(className: string): string { if (!this.classObjs.has(className)) { const meta = this.classMetaOf(className); - this.classObjs.set(className, { nameSym: this.internLiteral(meta.def.jsName ?? "") }); + this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) }); } return mangleClassObj(className); } diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index 6446a581b..b49c8895e 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -10,6 +10,7 @@ import { typeKey, type IrExpr, type IrFunction, + classValueConstructor, } from "../../ir/ir.js"; import { liveRefView } from "./expr-stream-bridges.js"; import { typedRefConstructor, vAdapters } from "./shapes.js"; @@ -1043,7 +1044,8 @@ export function emitCallExpr( if (e.callee.type.kind !== "classval") throw new InternalCompilerError("llvm emitter bug: newValue on non-classval callee"); const cls = e.callee.type.className; - const ctor = host.fnByName.get(`%${cls}.constructor`); + const valueCtor = classValueConstructor(host.classMetaOf(cls).def); + const ctor = host.fnByName.get(valueCtor.fn); if (!ctor) throw new InternalCompilerError( `llvm emitter bug: newValue on ${cls} without a constructor`, @@ -1056,7 +1058,7 @@ export function emitCallExpr( B.line(`${ctorp} = getelementptr inbounds %ScrClassObj, ptr ${callee.name}, i64 0, i32 3`); B.line(`${thunk} = load ptr, ptr ${ctorp}`); const argList = args - .map((a, i) => `${host.llType(ctor.params[i + 1]!.type)} ${a.name}`) + .map((a, i) => `${host.llType(ctor.params[i + valueCtor.firstArg]!.type)} ${a.name}`) .join(", "); const t = B.tmp(); B.line(`${t} = call ptr ${thunk}(ptr ${callee.name}${argList ? ", " + argList : ""})`); diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index f24db09cf..5e4db921c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -7,6 +7,7 @@ import { stripTypeCasts } from "./arguments.js"; import { streamClassAliasDecl } from "../lower-stream.js"; import { zlibClassName, zlibStreamModeOf } from "./compression.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; +import { RUNTIME_ERROR_CLASSES } from "../../../ir/ir.js"; /** A builtin class an expression names at compile time: the error * classes, node:events EventEmitter, the node:stream classes (each a @@ -123,6 +124,60 @@ export function registerBuiltinClassAlias( return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol); } +/** The runtime-provided class (EventEmitter, a stream class, an error + * class) `expr` names, when values of it construct (valueCtorParams), or + * null. */ +export function runtimeClassValueOf(lowerer: Lowerer, expr: ts.Expression): ClassInfo | null { + const ref = builtinClassRefOf(lowerer, expr); + if (ref === null || ref.kind === "zlib" || ref.kind === "collection") return null; + return ref.info.valueCtorParams !== undefined ? ref.info : null; +} + +/** The runtime-provided class whose static side `type` is (`typeof + * EventEmitter`, `typeof Readable`, `ErrorConstructor`), when values of + * it construct, or null. Instance types have no construct signatures and + * answer null. A union of such static sides (`[Readable, Writable]`'s + * element type) is their nearest common ancestor's value when every arm + * constructs through the same parameters: a slot of the ancestor holds + * any of them. */ +export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string | null { + if (type.isUnionType()) { + const arms: ClassInfo[] = []; + for (const part of ts.constituentTypes(type)) { + const name = runtimeClassValueOfType(lowerer, part); + const info = name !== null ? lowerer.classes.get(name) : undefined; + if (!info) return null; + arms.push(info); + } + const ancestor = commonRuntimeAncestor(arms); + if (ancestor === null) return null; + return arms.every((arm) => lowerer.ctorAbiEquals(arm, ancestor)) ? ancestor.def.name : null; + } + if (lowerer.checker.getConstructSignatures(type).length === 0) return null; + const symbol = type.getSymbol(); + if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; + let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol); + if (!info && symbol.name.endsWith("Constructor")) { + for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { + if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null; + } + } + return info?.valueCtorParams !== undefined ? info.def.name : null; +} + +/** The nearest class every one of `infos` is or extends, when it is a + * runtime class whose values construct, or null. */ +function commonRuntimeAncestor(infos: ClassInfo[]): ClassInfo | null { + for (let candidate = infos[0] ?? null; candidate; candidate = candidate.base) { + const holdsAll = infos.every((info) => { + for (let c: ClassInfo | null = info; c; c = c.base) if (c === candidate) return true; + return false; + }); + if (holdsAll) return candidate.def.runtime && candidate.valueCtorParams ? candidate : null; + } + return null; +} + /** True when two references name the same builtin class. */ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean { if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode; diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b3cd7e39b..5c1b47e6a 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -65,6 +65,7 @@ import { isUnitType, shapeHasAccessorSlots, typeEquals, + RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; @@ -8968,6 +8969,14 @@ export function lowerObjectMethodCall( ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + // EventEmitter's static side (the class, a value of it, or the events + // module, which is the class) keeps its static surface on the emitter + // dispatch (`EventEmitter.setMaxListeners(n)`), which tells the statics + // apart by the receiver's construct signatures. The stream classes + // inherit the same statics. + if (mappedReceiver?.kind === "classval" && mappedReceiver.className === RUNTIME_EMITTER_CLASS) { + mappedReceiver = { kind: "object", className: RUNTIME_EMITTER_CLASS }; + } // A specialized JS parameter can expose a native field whose checker // type still belongs to the unspecialized body (plane.normal.dot()). // Use the lowered receiver's representation and retain its evaluation. diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d44748084..093e7eeef 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -2,6 +2,7 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; import { checkedIterableSpread } from "./checked-iterable-spread.js"; import { lowerReadlineUiNew } from "./builtins/readline.js"; +import { checkedClassUnionAssertion } from "./class-assertions.js"; import { collectClassMethodMutations } from "./class-method-mutations.js"; import { InternalCompilerError } from "../../errors.js"; /* Class lowering: shape collection over the single-inheritance graph @@ -12,6 +13,7 @@ import { InternalCompilerError } from "../../errors.js"; import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js"; +import { everyStmtList } from "../../ir/traverse.js"; import { type BuiltinClassRef, builtinClassAliasOf, @@ -28,6 +30,7 @@ import { type IrClassDef, type IrExpr, type IrFunction, + type IrLibFn, type IrLocal, type IrParam, type IrStmt, @@ -37,6 +40,7 @@ import { RUNTIME_STREAM_CLASSES, STRING, type SrcLoc, + classValueConstructor, UNDEFINED_T, VOID, arrayOf, @@ -539,6 +543,11 @@ export interface ClassInfo { * inherited signature, and the synthesized constructor forwards to it * (forwarding the completed ABI values; defaults apply in the base). */ ctorParams: ParamShape[]; + /** A runtime-provided class whose values construct (`const kinds = + * [Readable, Writable]`, `(C: typeof EventEmitter) => new C()`): the + * parameters of its `%C.construct` function (classValueConstructor). + * Absent for runtime classes that are not values. */ + valueCtorParams?: ParamShape[]; base: ClassInfo | null; /** Family heritage resolves its type arguments separately for each instance. */ genericBase?: ts.ExpressionWithTypeArguments; @@ -782,6 +791,17 @@ function symbolSlotReturnType( return out; } +/** The error classes whose values construct through error.newOptions. */ +const ERROR_CLASS_VALUES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + /** The builtin Error hierarchy * as eagerly-registered ClassInfos: mapType names them the moment a lib * Error type appears, so the infos must exist before any lowering. They @@ -860,6 +880,17 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void { { type: DYN, mode: "omittable" }, { type: DYN, mode: "omittable" }, ], + // The ECMAScript error constructors take (message?, options?); + // DOMException, AggregateError, AssertionError and SuppressedError + // have their own construction and are not values. + ...(ERROR_CLASS_VALUES.has(irName) + ? { + valueCtorParams: [ + { type: DYN, mode: "omittable" as const }, + { type: DYN, mode: "omittable" as const }, + ], + } + : {}), base, subclasses: [], throwingSetters: [], @@ -892,6 +923,7 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void { // `new EventEmitter()` — zero-argument (the options bag fences at // construction sites; the checker may admit it via @types/node). ctorParams: [], + valueCtorParams: [], base: null, subclasses: [], throwingSetters: [], @@ -923,6 +955,9 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void { // `new Readable(opts?)` — the options bag is parsed structurally // by the stream spoke (lowerNew never completes against this). ctorParams: [], + // Construction through a class value builds the stream without + // options (lowerNew reports options passed through a value). + valueCtorParams: [], base, subclasses: [], throwingSetters: [], @@ -5059,11 +5094,20 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) ); } if (info.builtinError || info.builtinEmitter || info.builtinStream !== undefined) { - lowerer.unsupported( - "SC1090", - blame, - `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, - ); + if (info.valueCtorParams === undefined) { + lowerer.unsupported( + "SC1090", + blame, + `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, + ); + } + defineClassValueConstruct(lowerer, info); + return { + kind: "classRef", + className: info.def.name, + type: { kind: "classval", className: info.def.name }, + loc: locOf(blame), + }; } if ( lowerer.inheritsBuiltinEmitterCtor(info) || @@ -5090,14 +5134,92 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) }; } +/** Notes a runtime-provided class whose value is named, widened to, or + * constructed through (a no-op for program classes, whose constructor is + * their own). runtimeClassConstructs defines `%C.construct` for the + * noted classes the finished module still references. */ +export function defineClassValueConstruct(lowerer: Lowerer, info: ClassInfo): void { + if (!info.def.runtime || info.valueCtorParams === undefined) return; + lowerer.noteEdge(classValueConstructor(info.def).fn); + lowerer.runtimeClassValues.set(info.def.name, info); +} + +/** The `%C.construct` functions of the noted runtime classes that a + * classRef, newValue or classval upcast in `functions` names. Lowering can + * name a class speculatively and discard the expression, so the finished + * functions decide. */ +export function runtimeClassConstructs(lowerer: Lowerer, functions: IrFunction[]): IrFunction[] { + if (lowerer.runtimeClassValues.size === 0) return []; + const used = new Set(); + const note = (type: IrType): void => { + if (type.kind === "classval" && lowerer.runtimeClassValues.has(type.className)) + used.add(type.className); + }; + const visitor = { + expr: (e: IrExpr): boolean => { + if (e.kind === "classRef") note(e.type); + else if (e.kind === "newValue") note(e.callee.type); + else if (e.kind === "upcast") { + note(e.type); + note(e.value.type); + } + return true; + }, + stmt: (): boolean => true, + }; + for (const fn of functions) everyStmtList(fn.body, visitor); + return [...used] + .sort() + .map((name) => runtimeClassConstruct(lowerer.runtimeClassValues.get(name)!)); +} + +/** `%C.construct` for a runtime-provided class value: the thunk's target, + * which builds the instance through the runtime constructor the direct + * `new C(...)` uses (emitter.new, a stream's .newDyn without options, + * error.newOptions). */ +function runtimeClassConstruct(info: ClassInfo): IrFunction { + const params = info.valueCtorParams ?? []; + const name = classValueConstructor(info.def).fn; + const loc = info.def.loc; + const type: IrType = { kind: "object", className: info.def.name }; + const ids = params.map((_, i) => `a${i}.0`); + const args = ids.map((id) => varRef(id, DYN, loc)); + let fn: IrLibFn = "error.newOptions"; + if (info.builtinEmitter) fn = "emitter.new"; + else if (info.builtinStream !== undefined) { + fn = `${streamCtorShape(info.def.name).fn}.newDyn` as IrLibFn; + args.push(dynUndefinedExpr(loc)); + } + return { + name, + params: params.map((shape, i) => ({ localId: ids[i]!, name: `a${i}`, type: shape.type })), + returnType: type, + locals: params.map((shape, i) => ({ + id: ids[i]!, + name: `a${i}`, + type: shape.type, + mutable: false, + })), + body: [{ kind: "return", value: { kind: "libCall", fn, args, type, loc }, loc }], + loc, + }; +} + +/** The constructor parameters construction through a class value completes + * against: a program class's constructor, or a runtime class's + * `%C.construct`. */ +function classValueParams(info: ClassInfo): ParamShape[] { + return info.def.runtime ? (info.valueCtorParams ?? info.ctorParams) : info.ctorParams; +} + /** Constructor-ABI equality — the classval widening rule: a classval(D) * value may flow into a classval(C) slot only when D's completed * constructor signature equals C's (same count, modes, and ABI types), * which is what keeps newValue completion against C's one signature * sound for every value legally in the slot. */ export function ctorAbiEquals(lowerer: Lowerer, sub: ClassInfo, sup: ClassInfo): boolean { - const a = sub.ctorParams; - const b = sup.ctorParams; + const a = classValueParams(sub); + const b = classValueParams(sup); return ( a.length === b.length && a.every((p, i) => p.mode === b[i]!.mode && typeEquals(p.type, b[i]!.type)) @@ -6232,6 +6354,9 @@ export function lowerClassValueProperty( const loc = locOf(expr); const member = expr.name.text; const info = lowerer.classes.get(recvT.className); + // A runtime-provided class value answers `.name` from its class object; + // its other statics (`Error.stackTraceLimit`) keep their own lowerings. + if (info?.def.runtime && member !== "name") return null; if ( info && ((hasRuntimeStatics(info) && !findStaticOn(lowerer, info, member)?.field) || @@ -9777,24 +9902,51 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee); if (callee.type.kind !== "classval") lowerer.badType(expr.expression, lowerer.typeOf(expr.expression)); - lowerer.noteEdge(`%${info.def.name}.constructor`); + lowerer.noteEdge(classValueConstructor(info.def).fn); // Every constructor a value in this slot can dispatch to is a // descendant's — mark them reachable like a virtual edge. const below = (c: ClassInfo): void => { for (const s of c.subclasses) { - lowerer.noteEdge(`%${s.def.name}.constructor`); + lowerer.noteEdge(classValueConstructor(s.def).fn); below(s); } }; below(info); - const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); - return { + // EventEmitter and stream class values construct without options + // only: a slot of either may hold any of those classes, and the + // direct `new Readable({...})` lowering reads its options at compile + // time (EventEmitter's options have no lowering at all). + if ( + (info.builtinStream !== undefined || info.builtinEmitter) && + (expr.arguments?.length ?? 0) > 0 + ) { + lowerer.unsupported( + "SC1090", + expr, + `EventEmitter and stream class values constructed with options (construct with 'new ${info.def.name.slice(1)}({ ... })' directly)`, + ); + } + defineClassValueConstruct(lowerer, info); + const args = lowerer.completeArgs(expr.arguments ?? [], classValueParams(info), loc, expr); + const built: IrExpr = { kind: "newValue", callee, args, type: { kind: "object", className: info.def.name }, loc, }; + // A runtime class slot widened from a union (`[Readable, Writable]`'s + // elements hold EventEmitter values): the checker types the result as + // the union of the arms' instances, which the checked class view + // answers from the instance's class. + if (info.def.runtime) { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (resultT?.kind === "union") { + const asserted = checkedClassUnionAssertion(lowerer, built, resultT, loc); + if (asserted) return asserted; + } + } + return built; } if (calleeT?.kind === "union") { const def = lowerer.unions.get(calleeT.unionId); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4b8ce648..d46226384 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -39,6 +39,8 @@ import { lowerZlibStreamBytesWritten, } from "./builtins/compression.js"; import { + runtimeClassValueOf, + runtimeClassValueOfType, builtinClassAliasOwnsNoSlot, builtinClassRefOf, sameBuiltinClass, @@ -940,6 +942,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { `a builtin function alias as an escaping value (call '${expr.text}' directly)`, ); } + // `EventEmitter`, `Readable` or `TypeError` (or a const alias of + // one) as a value in a TypeScript file: the runtime class's class + // object. JavaScript files keep the identity token. + if (!isJsSourceFile(expr.getSourceFile())) { + const runtimeClass = runtimeClassValueOf(lowerer, expr); + if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr); + } // `const E = EventEmitter` owns no runtime slot: construction, // instanceof, typeof and .name lower at their use sites. if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) { @@ -1962,6 +1971,16 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ref = builtinClassRefOf(lowerer, expr.expression); if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc }; } + // `C.name` through a runtime-provided class VALUE (a `typeof Readable` + // parameter): the class object's stored name. + if ( + expr.name.text === "name" && + !expr.questionDotToken && + runtimeClassValueOfType(lowerer, lowerer.typeOf(expr.expression)) !== null + ) { + const read = lowerer.lowerFieldRead(expr); + if (read) return read; + } // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. const staticField = lowerStaticFieldRead(lowerer, expr); @@ -13013,9 +13032,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc } const left = lowerer.lowerExpr(expr.left); if (left.type.kind === "dyn") { + if (targetInfo.def.runtime) { + lowerer.unsupported( + "SC1090", + expr, + "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", + ); + } const classValue = lowerer.lowerExpr(expr.right); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } + // A union of class instances against a runtime class value (`new K()` + // through a widened `[Readable, Writable]` element). + if (left.type.kind === "union" && targetInfo.def.runtime) { + const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); + if (tested) return tested; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -13033,6 +13065,15 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const classValue = lowerer.lowerExpr(expr.right); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const writableTest = writableClassValueTest( + lowerer, + left, + classValue, + targetInfo, + lhsInfo, + loc, + ); + if (writableTest) return writableTest; return { kind: "instanceOfValue", value: left, classValue, type: BOOL, loc }; } // A standalone side: the answer is static (the target slot can @@ -13331,6 +13372,137 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ); } +/** `x instanceof K` for a union-typed operand whose arms are class + * instances or unit values: each instance arm tests against the class + * value (with the Writable rule), unit arms answer false. Null when an arm + * is anything else. */ +function unionClassValueInstanceOf( + lowerer: Lowerer, + left: IrExpr, + right: ts.Expression, + targetInfo: ClassInfo, + loc: SrcLoc, +): IrExpr | null { + if (left.type.kind !== "union") return null; + const unionId = left.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if ( + !arms?.every((arm) => { + if (arm.kind !== "object") return isUnitType(arm); + const info = lowerer.classes.get(arm.className); + return info !== undefined && lowerer.inHierarchy(info); + }) + ) + return null; + const classValue = lowerer.lowerExpr(right); + if (classValue.type.kind !== "classval") lowerer.badType(right, lowerer.typeOf(right)); + const value = lowerer.declareHiddenLocal("%instanceValue", left.type); + const cls = lowerer.declareHiddenLocal("%instanceClass", classValue.type); + const valueRef = varRef(value.id, left.type, loc); + const clsRef = varRef(cls.id, classValue.type, loc); + let result: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; + arms.forEach((arm, tag) => { + if (arm.kind !== "object") return; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: valueRef, type: arm, loc }; + const lhsInfo = lowerer.classes.get(arm.className)!; + const test: IrExpr = writableClassValueTest( + lowerer, + narrowed, + clsRef, + targetInfo, + lhsInfo, + loc, + ) ?? { kind: "instanceOfValue", value: narrowed, classValue: clsRef, type: BOOL, loc }; + result = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: valueRef, type: BOOL, loc }, + then: test, + else_: result, + type: BOOL, + loc, + }; + }); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: value.id, init: left, loc }, + { kind: "varDecl", localId: cls.id, init: classValue, loc }, + ], + result, + type: BOOL, + loc, + }; +} + +/** `x instanceof K` where K is a class value that may hold Writable (a + * `typeof Writable` or `typeof EventEmitter` slot): Node's + * Writable[Symbol.hasInstance] also answers true for streams with a + * writable side that do not inherit from Writable (Duplex and its + * subclasses), so when K is Writable the Duplex interval answers too. + * Null when the slot cannot hold Writable or the operand cannot be a + * Duplex. */ +function writableClassValueTest( + lowerer: Lowerer, + left: IrExpr, + classValue: IrExpr, + targetInfo: ClassInfo, + lhsInfo: ClassInfo, + loc: SrcLoc, +): IrExpr | null { + const writable = lowerer.classes.get("%Writable"); + const duplex = lowerer.classes.get("%Duplex"); + if (!writable || !duplex || classValue.type.kind !== "classval" || left.type.kind !== "object") + return null; + const slotHoldsWritable = + targetInfo === writable || lowerer.isSubclassOf("%Writable", targetInfo.def.name); + const operandMayBeDuplex = + lhsInfo === duplex || + lowerer.isSubclassOf(lhsInfo.def.name, "%Duplex") || + lowerer.isSubclassOf("%Duplex", lhsInfo.def.name); + if (!slotHoldsWritable || !operandMayBeDuplex) return null; + lowerer.defineClassValueConstruct(writable); + const value = lowerer.declareHiddenLocal("%instanceValue", left.type); + const cls = lowerer.declareHiddenLocal("%instanceClass", classValue.type); + const valueRef = varRef(value.id, left.type, loc); + const clsRef = varRef(cls.id, classValue.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: value.id, init: left, loc }, + { kind: "varDecl", localId: cls.id, init: classValue, loc }, + ], + result: { + kind: "logical", + op: "||", + left: { kind: "instanceOfValue", value: valueRef, classValue: clsRef, type: BOOL, loc }, + right: { + kind: "logical", + op: "&&", + left: { + kind: "bin", + op: "===", + left: clsRef, + right: { + kind: "classRef", + className: "%Writable", + type: { kind: "classval", className: "%Writable" }, + loc, + }, + type: BOOL, + loc, + }, + right: { kind: "instanceOf", value: valueRef, className: "%Duplex", type: BOOL, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; +} + /** A statically decided `instanceof` still reads its class after the left * operand: a class with an initialization flag throws there before its * declaration ran. */ diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 700f68a2e..3a0958ae6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6870,7 +6870,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm } } else { const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.left.expression)); - if (recvT?.kind === "classval") { + // A runtime-provided class (`Error.stackTraceLimit = 0`) keeps + // its static lowering below. + if (recvT?.kind === "classval" && !lowerer.classes.get(recvT.className)?.def.runtime) { // A static write through a class VALUE: sound exactly when // the value can only BE the declaring class — a LEAF class // (no strict descendants can flow into the slot, so JS's diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 2fb266be0..1873ac361 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -269,7 +269,11 @@ import { stdlibGlobalMember, nodeTypesOnlySymbol, } from "./surfaces.js"; -import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js"; +import { + type BuiltinClassRef, + builtinClassAliasOf, + runtimeClassValueOfType, +} from "./builtins/class-values.js"; import { type FileParts, splitFiles, @@ -302,6 +306,8 @@ import { type ClassIteratorInfo, type GenericClassInfo, registerBuiltinErrorClasses, + defineClassValueConstruct, + runtimeClassConstructs, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, @@ -2774,6 +2780,9 @@ export class Lowerer { // top level, top = the innermost nested function currently lowering). fnStack: FnCtx[] = []; readonly liftedFns: IrFunction[] = []; + /** Runtime-provided classes used as values (defineClassValueConstruct), + * by IR class name. */ + readonly runtimeClassValues = new Map(); /** 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; @@ -3004,6 +3013,7 @@ export class Lowerer { isErasing: () => this.erasingDepth > 0, genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), + runtimeClassValue: (type) => runtimeClassValueOfType(this, type), mixinClassInstance: (decl) => this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } @@ -3217,6 +3227,10 @@ export class Lowerer { return builtinClassAliasOf(this, symbol); } + defineClassValueConstruct(info: ClassInfo): void { + defineClassValueConstruct(this, info); + } + /** Program-wide qualified name for a top-level declaration. */ qualify(sf: ts.SourceFile, name: string): string { return `${this.fileTag.get(sf) ?? ""}${name}`; @@ -6744,6 +6758,7 @@ export class Lowerer { // implicit-any instances lowered eagerly at their first call sites. functions.push(...this.liftedFns); functions.push(...this.implicitFns); + functions.push(...runtimeClassConstructs(this, functions)); if (this.remainder) { // Deferred collection diagnostics nothing flushed — declarations no @@ -7419,6 +7434,7 @@ export class Lowerer { ...this.liftedFns, ...this.implicitFns, ]; + functions.push(...runtimeClassConstructs(this, functions)); if (this.regexGroupResults) for (const fn of functions) foldRegexSpanReads(this, fn); // The worklist can leave a virtual site with a single implementation. // Devirtualize after construction/override reachability has settled. @@ -8552,6 +8568,18 @@ export class Lowerer { loc: expr.loc, }; } + // A runtime-provided class value has no checked-dynamic form that + // inspects as Node's does (`[Function: EventEmitter] { ... }`). + if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) { + this.pushDiag( + unsupportedDiag( + "SC1090", + expr.loc, + `a runtime-provided class value ('${expr.type.className.slice(1)}') as an untyped value`, + ), + ); + throw new PoisonError(); + } if (expr.kind === "unitLit" || this.dynConvertible(expr.type)) { // Records and arrays keep their identity as untyped values, including // when they arrive through a union, so `unknown` views, weak @@ -8771,6 +8799,7 @@ export class Lowerer { // and no completion target is meaningful — the exact-shape fence // downstream names the class-value flow instead. if (sub && sup && !sup.generic && this.ctorAbiEquals(sub, sup)) { + defineClassValueConstruct(this, sup); return { kind: "upcast", value: expr, type: expected, loc: expr.loc }; } } @@ -8891,6 +8920,7 @@ export class Lowerer { }); if (baseTag >= 0) { if (!sub || !this.ctorAbiEquals(sub, c)) break; + defineClassValueConstruct(this, c); const widened: IrExpr = { kind: "upcast", value: expr, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 41d18a6a2..b7fa17b7b 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1055,6 +1055,9 @@ export interface TypeMapperCtx { * mixin context (the type alone cannot name a call site). */ mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null; localClassInstance?: (decl: ts.ClassLikeDeclaration, instanceType?: ts.Type) => IrType | null; + /** The runtime-provided class whose static side a type is (`typeof + * EventEmitter`, `ErrorConstructor`), when values of it construct. */ + runtimeClassValue?: (type: ts.Type) => string | null; /** Live class protocols retain identity and receiver dispatch rather than * copying an instance into an unrelated record layout. */ classImplementedProtocol?: (type: ts.Type) => boolean; @@ -2047,6 +2050,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } return { kind: "object", className: classNamer(classDecl) }; } + // A runtime-provided class's static side (`typeof EventEmitter`, + // `typeof Readable`, `ErrorConstructor`): that class's value. + { + const runtimeClass = ctx.runtimeClassValue?.(widened); + if (runtimeClass) return { kind: "classval", className: runtimeClass }; + } // The class STATIC side (`typeof C` — the class name as a value): the // same symbol WITH its construct signatures. Program classes only; lib // and package statics keep their fences. diff --git a/packages/compiler/src/ir/analysis.ts b/packages/compiler/src/ir/analysis.ts index 88f6f394f..4fa2d8361 100644 --- a/packages/compiler/src/ir/analysis.ts +++ b/packages/compiler/src/ir/analysis.ts @@ -11,6 +11,7 @@ import { type IrStmt, type IrType, type IrUnionDef, + classValueConstructor, } from "./ir.js"; /** Destination tags for a union widening that preserves every payload's @@ -313,7 +314,7 @@ export function newValueMayThrow( if (!meta) throw new InternalCompilerError(`IR analysis bug: newValue on unknown class ${className}`); const any = (node: IrClassGraphNode): boolean => - mayThrow.has(`%${node.def.name}.constructor`) || node.children.some(any); + mayThrow.has(classValueConstructor(node.def).fn) || node.children.some(any); return any(meta); } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 87a4de606..6c8df7733 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1494,6 +1494,27 @@ export const RUNTIME_ERROR_CLASSES: ReadonlyMap< * owns listener closures, whatever the subclass fields say. */ export const RUNTIME_EMITTER_CLASS = "%EventEmitter"; +/** The function that construction through a class value calls, and the + * index of its first argument. A program class constructs through + * `%C.constructor(this, ...)` after the thunk allocates; a runtime-provided + * class (EventEmitter, the stream and error classes) through the + * frontend's `%C.construct(...)`, which allocates through the runtime and + * returns the new instance. */ +export function classValueConstructor(def: { name: string; runtime?: true }): { + fn: string; + firstArg: number; +} { + return def.runtime + ? { fn: `%${def.name}.construct`, firstArg: 0 } + : { fn: `%${def.name}.constructor`, firstArg: 1 }; +} + +/** The JavaScript `.name` of a class: its jsName, or the runtime-provided + * class's own name (`%TypeError` is TypeError). */ +export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string { + return def.jsName ?? (def.runtime ? def.name.replace(/^%/, "") : ""); +} + /** The runtime-provided node:stream classes (ScrStream / scr_stream_*, * scr_stream.c — link-gated by moduleUsesStream). They root at the * emitter class (base chains below), so the emitter method surface and diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 44b6814db..5cf262e1f 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -56,7 +56,7 @@ import { maybePromiseUnionArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js"; +import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -2937,15 +2937,21 @@ function validateFunction( err(`upcast: "${sub}" does not extend "${sup}"`, e.loc); break; } - const subCtor = functions.get(`%${sub}.constructor`); - const supCtor = functions.get(`%${sup}.constructor`); - if (!subCtor || !supCtor) { + const subClass = classes.get(sub); + const supClass = classes.get(sup); + const subValueCtor = subClass ? classValueConstructor(subClass) : null; + const supValueCtor = supClass ? classValueConstructor(supClass) : null; + const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined; + const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined; + if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) { err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc); break; } + const subArgs = subCtor.params.slice(subValueCtor.firstArg); + const supArgs = supCtor.params.slice(supValueCtor.firstArg); const abiEqual = - subCtor.params.length === supCtor.params.length && - subCtor.params.every((p, i) => i === 0 || typeEquals(p.type, supCtor.params[i]!.type)); + subArgs.length === supArgs.length && + subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)); if (!abiEqual) { err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc); } @@ -2970,16 +2976,13 @@ function validateFunction( err(`classRef to undeclared class "${e.className}"`, e.loc); break; } - if (cls.runtime) { - err(`classRef to runtime-provided class "${e.className}"`, e.loc); - } - if (cls.jsName === undefined) { + if (cls.jsName === undefined && !cls.runtime) { err(`classRef to "${e.className}" without a jsName (the class object's .name)`, e.loc); } // A class value can always be constructed through — the frontend // notes the constructor edge at every classRef, so the emitted // thunk has a body to call. - if (!functions.has(`%${e.className}.constructor`)) { + if (!functions.has(classValueConstructor(cls).fn)) { err(`classRef to "${e.className}" without its constructor function`, e.loc); } if (!typeEquals(e.type, { kind: "classval", className: e.className })) { @@ -3008,21 +3011,24 @@ function validateFunction( break; } const cls = e.callee.type.className; - const ctor = functions.get(`%${cls}.constructor`); - if (!ctor) { + const calleeClass = classes.get(cls); + const valueCtor = calleeClass ? classValueConstructor(calleeClass) : null; + const ctor = valueCtor ? functions.get(valueCtor.fn) : undefined; + if (!ctor || !valueCtor) { err(`newValue on "${cls}": missing constructor function`, e.loc); break; } // Count-exact against the static class's completed ABI (every // value legally in the slot shares it — the upcast rule). - if (ctor.params.length !== e.args.length + 1) { + const first = valueCtor.firstArg; + if (ctor.params.length !== e.args.length + first) { err( - `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - 1}`, + `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - first}`, e.loc, ); } e.args.forEach((a, i) => { - const p = ctor.params[i + 1]; + const p = ctor.params[i + first]; if (p) expectType(a, p.type, `newValue on "${cls}" arg ${i}`); }); if (!typeEquals(e.type, { kind: "object", className: cls })) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json new file mode 100644 index 000000000..6703b89ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5981-runtime-class-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5981-runtime-class-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json new file mode 100644 index 000000000..a9aeabf71 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5982-runtime-class-value-modules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5982-runtime-class-value-modules/kinds.ts", + "/tests/corpus/5982-runtime-class-value-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json new file mode 100644 index 000000000..eded433eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/runtime-class-value-fences.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/runtime-class-value-fences.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5981-runtime-class-values.ts b/tests/corpus/5981-runtime-class-values.ts new file mode 100644 index 000000000..b2ebec988 --- /dev/null +++ b/tests/corpus/5981-runtime-class-values.ts @@ -0,0 +1,117 @@ +// EventEmitter, the stream classes and the error classes as values: +// parameters, arrays, Map values and object fields hold the class, and +// construction, instanceof, typeof, .name and === through the value +// answer as in Node. +import { EventEmitter } from "node:events"; +import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream"; + +function makeEmitter(C: typeof EventEmitter): EventEmitter { + return new C(); +} +const emitter = makeEmitter(EventEmitter); +emitter.on("ping", (n: number) => console.log("ping", n)); +emitter.emit("ping", 1); +console.log("emitter", emitter instanceof EventEmitter); + +const E: typeof EventEmitter = EventEmitter; +console.log("emitter value", typeof E, E.name, E === EventEmitter, new E() instanceof E); +const emitterKinds: (typeof EventEmitter)[] = [EventEmitter, PassThrough]; +for (const Kind of emitterKinds) { + const made = new Kind(); + console.log("emitter kind", Kind.name, made instanceof EventEmitter, made instanceof Readable); +} + +const streamKinds: (typeof Readable)[] = [Readable, Duplex, Transform, PassThrough]; +for (const Kind of streamKinds) { + const stream = new Kind(); + console.log( + "stream", + Kind.name, + stream instanceof Kind, + stream instanceof Readable, + stream instanceof Duplex, + stream instanceof Writable, + ); +} + +const W: typeof Writable = Writable; +const pass = new PassThrough(); +console.log("writable value", pass instanceof W, new Readable() instanceof W, new W() instanceof W); + +function writableKind(): typeof Writable { + return Writable; +} +console.log("computed", pass instanceof writableKind(), new Readable() instanceof writableKind()); + +const mixedKinds = [Readable, Writable]; +for (const Kind of mixedKinds) { + const made = new Kind(); + console.log( + "mixed", + Kind.name, + typeof Kind, + made instanceof Kind, + made instanceof EventEmitter, + pass instanceof Kind, + ); +} +const emitterMix = [EventEmitter, Readable]; +const SecondKind = emitterMix[1]; +console.log("emitter mix", emitterMix[0] === EventEmitter, new SecondKind() instanceof Readable); + +const readable = new Readable(); +readable._read = () => {}; +readable.on("data", (chunk: Buffer) => console.log("data", chunk.toString())); +readable.push("chunk"); +readable.push(null); + +const errorKinds: ErrorConstructor[] = [Error, TypeError, RangeError, SyntaxError]; +for (const Kind of errorKinds) { + const err = new Kind("failed", { cause: Kind.name }); + console.log( + "error", + Kind.name, + err.name, + err.message, + err.cause, + err instanceof Kind, + err instanceof Error, + err instanceof TypeError, + ); +} + +function raise(Kind: ErrorConstructor, message: string): never { + throw new Kind(message); +} +try { + raise(RangeError, "out of range"); +} catch (err) { + console.log("caught", err instanceof RangeError, (err as Error).message, String(err)); +} + +const byName = new Map([ + ["type", TypeError], + ["reference", ReferenceError], +]); +const ReferenceKind = byName.get("reference")!; +console.log("map value", ReferenceKind.name, new ReferenceKind() instanceof ReferenceError); + +const registry: { stream: typeof Readable; error: ErrorConstructor } = { + stream: Transform, + error: URIError, +}; +console.log( + "fields", + registry.stream.name, + new registry.stream() instanceof Transform, + new registry.error("bad uri").name, +); + +const EmitterAlias = EventEmitter; +const aliasKinds = [EmitterAlias]; +let mutableKind = EventEmitter; +console.log("aliases", aliasKinds.length, typeof mutableKind, mutableKind === EmitterAlias); + +const sameClass = errorKinds[1] === TypeError; +const firstKind = streamKinds[0]; +console.log("identity", sameClass, firstKind === Readable, firstKind === Duplex); diff --git a/tests/corpus/5982-runtime-class-value-modules/kinds.ts b/tests/corpus/5982-runtime-class-value-modules/kinds.ts new file mode 100644 index 000000000..68a5f6d93 --- /dev/null +++ b/tests/corpus/5982-runtime-class-value-modules/kinds.ts @@ -0,0 +1,14 @@ +import { EventEmitter } from "node:events"; +import { PassThrough, Readable, Transform } from "node:stream"; + +export const errorKinds: ErrorConstructor[] = [TypeError, RangeError]; +export const streamKinds: (typeof Readable)[] = [PassThrough, Transform]; + +export function build(Kind: typeof EventEmitter): EventEmitter { + return new Kind(); +} + +export function describe(Kind: ErrorConstructor, message: string): string { + const err = new Kind(message); + return `${Kind.name}: ${err.message} (${err instanceof Kind})`; +} diff --git a/tests/corpus/5982-runtime-class-value-modules/main.ts b/tests/corpus/5982-runtime-class-value-modules/main.ts new file mode 100644 index 000000000..0b9ea4cd3 --- /dev/null +++ b/tests/corpus/5982-runtime-class-value-modules/main.ts @@ -0,0 +1,16 @@ +// Runtime class values exported from one module and used in another: +// arrays of error and stream classes, and functions taking a class. +import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; +import { build, describe, errorKinds, streamKinds } from "./kinds.ts"; +import * as kinds from "./kinds.ts"; + +for (const Kind of errorKinds) console.log(describe(Kind, "from main")); +for (const Kind of kinds.streamKinds) { + const stream = new Kind(); + console.log(Kind.name, stream instanceof Kind, stream instanceof Readable); +} +const emitter = build(EventEmitter); +emitter.on("done", () => console.log("done")); +emitter.emit("done"); +console.log(streamKinds.length, errorKinds[0] === TypeError, kinds.errorKinds[1] === RangeError); diff --git a/tests/diagnostics/builtin-class-alias-escape.ts b/tests/diagnostics/builtin-class-alias-escape.ts index b3f9be5d3..29186738c 100644 --- a/tests/diagnostics/builtin-class-alias-escape.ts +++ b/tests/diagnostics/builtin-class-alias-escape.ts @@ -1,15 +1,7 @@ -// A builtin class alias owns no runtime value in a TypeScript file: +// A Map or Set alias owns no runtime value in a TypeScript file: // construction, instanceof, typeof and .name compile, other uses report -// at the use. A let binding of a class is not an alias. -import { EventEmitter } from "node:events"; - -const Emitter = EventEmitter; -const kinds = [Emitter]; -console.log(kinds.length); - +// at the use. Aliases of EventEmitter, the stream classes and the error +// classes are class values. const M = Map; const boxed: unknown = M; console.log(boxed !== undefined); - -let Mutable = EventEmitter; -console.log(typeof Mutable); diff --git a/tests/diagnostics/class-values.ts b/tests/diagnostics/class-values.ts index 3a53e5185..4e5a6ea4c 100644 --- a/tests/diagnostics/class-values.ts +++ b/tests/diagnostics/class-values.ts @@ -18,9 +18,9 @@ class Unrelated { static tag = "unrelated"; } -// Builtin classes as values: a const alias resolves at compile time, and -// an array of builtin classes has no element type. -const E = Error; +// Builtin classes with their own construction as values (Error and its +// standard subclasses are values; AggregateError is not). +const E = AggregateError; const errorClasses = [E]; // Classes extending builtin bases as values (the error-message completion diff --git a/tests/diagnostics/runtime-class-value-fences.ts b/tests/diagnostics/runtime-class-value-fences.ts new file mode 100644 index 000000000..3404eab13 --- /dev/null +++ b/tests/diagnostics/runtime-class-value-fences.ts @@ -0,0 +1,23 @@ +// Values of EventEmitter, the stream classes and the error classes +// construct, test instanceof and answer typeof, .name and ===. These uses +// report at the use. +import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; + +const R: typeof Readable = Readable; +const configured = new R({ read() {} }); +console.log(configured.readable); + +const C: typeof EventEmitter = EventEmitter; +console.log(C); +const boxed: unknown = Readable; +console.log(boxed !== undefined, C.defaultMaxListeners); + +// TypeScript types this array as RangeErrorConstructor[] (the error +// constructor interfaces are structurally identical); annotate it as +// ErrorConstructor[]. +const errorMix = [TypeError, RangeError]; +console.log(errorMix.length); + +const aggregates = [AggregateError]; +console.log(aggregates.length); diff --git a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt index 9d1d981e2..bf2c56649 100644 --- a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt +++ b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt @@ -1,22 +1,6 @@ -builtin-class-alias-escape.ts:7:16 - error SC1090: a builtin class alias as an escaping value (use 'Emitter' with new, instanceof, typeof or .name) is not supported yet +builtin-class-alias-escape.ts:6:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet - 6 | const Emitter = EventEmitter; - 7 | const kinds = [Emitter]; - | ^~~~~~~ - 8 | console.log(kinds.length); - -builtin-class-alias-escape.ts:11:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet - - 10 | const M = Map; - 11 | const boxed: unknown = M; - | ^ - 12 | console.log(boxed !== undefined); - -builtin-class-alias-escape.ts:14:15 - error SC2020: 'events.EventEmitter' is part of the standard library types but has no scriptc lowering yet - - 13 | - 14 | let Mutable = EventEmitter; - | ^~~~~~~~~~~~ - 15 | console.log(typeof Mutable); - - 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 + 5 | const M = Map; + 6 | const boxed: unknown = M; + | ^ + 7 | console.log(boxed !== undefined); \ No newline at end of file diff --git a/tests/harness/__snapshots__/class-values.ts.txt b/tests/harness/__snapshots__/class-values.ts.txt index 953d1c2d1..af1dd7a13 100644 --- a/tests/harness/__snapshots__/class-values.ts.txt +++ b/tests/harness/__snapshots__/class-values.ts.txt @@ -1,15 +1,15 @@ -class-values.ts:24:7 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile +class-values.ts:24:7 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile - 23 | const E = Error; + 23 | const E = AggregateError; 24 | const errorClasses = [E]; | ^~~~~~~~~~~~ 25 | 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') -class-values.ts:24:22 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile +class-values.ts:24:22 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile - 23 | const E = Error; + 23 | const E = AggregateError; 24 | const errorClasses = [E]; | ^~~ 25 | diff --git a/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt b/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt new file mode 100644 index 000000000..26a4f8c25 --- /dev/null +++ b/tests/harness/__snapshots__/runtime-class-value-fences.ts.txt @@ -0,0 +1,63 @@ +runtime-class-value-fences.ts:8:20 - error SC1090: EventEmitter and stream class values constructed with options (construct with 'new Readable({ ... })' directly) are not supported yet + + 7 | const R: typeof Readable = Readable; + 8 | const configured = new R({ read() {} }); + | ^~~~~~~~~~~~~~~~~~~~ + 9 | console.log(configured.readable); + +runtime-class-value-fences.ts:12:13 - error SC1090: console.log of 'typeof EventEmitter' values ('typeof EventEmitter' values have no inspect lowering) are not supported yet + + 11 | const C: typeof EventEmitter = EventEmitter; + 12 | console.log(C); + | ^ + 13 | const boxed: unknown = Readable; + +runtime-class-value-fences.ts:13:24 - error SC1090: a runtime-provided class value ('Readable') as an untyped value is not supported yet + + 12 | console.log(C); + 13 | const boxed: unknown = Readable; + | ^~~~~~~~ + 14 | console.log(boxed !== undefined, C.defaultMaxListeners); + +runtime-class-value-fences.ts:14:34 - error SC2020: 'typeof EventEmitter.defaultMaxListeners' is part of the standard library types but has no scriptc lowering yet + + 13 | const boxed: unknown = Readable; + 14 | console.log(boxed !== undefined, C.defaultMaxListeners); + | ^~~~~~~~~~~~~~~~~~~~~ + 15 | + + hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) + +runtime-class-value-fences.ts:19:19 - error SC1090: structurally-typed class-value flows ('typeof TypeError' into a 'typeof RangeError' slot: only a class and its subclasses share a slot — extend the base class) are not supported yet + + 18 | // ErrorConstructor[]. + 19 | const errorMix = [TypeError, RangeError]; + | ^~~~~~~~~ + 20 | console.log(errorMix.length); + +runtime-class-value-fences.ts:22:7 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile + + 21 | + 22 | const aggregates = [AggregateError]; + | ^~~~~~~~~~ + 23 | console.log(aggregates.length); + + 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') + +runtime-class-value-fences.ts:22:20 - error SC2009: values of type 'AggregateErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'AggregateErrorConstructor' does not compile + + 21 | + 22 | const aggregates = [AggregateError]; + | ^~~~~~~~~~~~~~~~ + 23 | console.log(aggregates.length); + + 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') + +runtime-class-value-fences.ts:23:13 - error SC2020: 'AggregateErrorConstructor[].length' is part of the standard library types but has no scriptc lowering yet + + 22 | const aggregates = [AggregateError]; + 23 | console.log(aggregates.length); + | ^~~~~~~~~~~~~~~~~ + 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 diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index d185f09e6..62703f6a4 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2216,6 +2216,8 @@ "5977-builtin-class-aliases.cjs": 2950, "5978-builtin-class-alias-modules/main.ts": 2750, "5979-writable-instanceof-duplex.ts": 2700, + "5981-runtime-class-values.ts": 2750, + "5982-runtime-class-value-modules/main.ts": 2500, "5990-worker-uncloneable.mjs": 3283, "5991-worker-async-dispose.mjs": 5699, "5992-worker-thread-name-lifetime.mjs": 2687, @@ -5303,6 +5305,8 @@ "5977-builtin-class-aliases.cjs": 4300, "5978-builtin-class-alias-modules/main.ts": 4450, "5979-writable-instanceof-duplex.ts": 6000, + "5981-runtime-class-values.ts": 6900, + "5982-runtime-class-value-modules/main.ts": 3400, "5990-worker-uncloneable.mjs": 22432, "5991-worker-async-dispose.mjs": 25768, "5992-worker-thread-name-lifetime.mjs": 28932, From e82686eab77beddf10e9a880665161bea60017f3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:29:28 -0500 Subject: [PATCH 66/67] Add getTestContext to node:test node:test changes, matching Node 24.22: - getTestContext() returns the context of the test, suite or hook whose code is running: undefined at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body. In the afterEach and t.after hooks of a subtest cancelled by its parent's timeout, it returns each hook's owner. - The context rides the AsyncLocalStorage snapshot under a reserved id, so it follows a test across its awaits, into the async functions, promise callbacks, timers and other callbacks it schedules, and into concurrent subtests, which each see their own. - The root's fullName is "", as its name is. - Optional chains over a test context, such as getTestContext()?.name, lower. AsyncLocalStorage enter and restore move into the always-linked async core so the test runner can use them. The snapshot holds the node's index in a table of live nodes, so a snapshot that outlives its node answers undefined. The get-test-context and get-test-context-cancelled fixtures compare the contexts seen at each point with Node, plain and with SCRIPTC_SAN=1. --- .../limitations/builtin-apis/test-runner.mdx | 1 + .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 20 +-- .../generated/node-v24-internal.json | 15 ++- internal/compatibility/static-support.json | 1 + .../ambient/scriptc-node-fallback.d.ts | 4 + .../compiler/src/backend/llvm/lib-test.ts | 4 + .../src/frontend/lowering/lower-test.ts | 46 ++++++- .../compiler/src/ir/builtin-signatures.ts | 4 + packages/compiler/src/ir/runtime-features.ts | 6 +- packages/runtime/src/scr_async.c | 55 +++++++- packages/runtime/src/scr_async.h | 2 + packages/runtime/src/scr_async_dyn.c | 39 ------ packages/runtime/src/scr_test.c | 84 ++++++++++-- packages/runtime/src/scr_test.h | 4 + .../cases/get-test-context-cancelled.ts | 40 ++++++ .../node-test/cases/get-test-context.ts | 122 ++++++++++++++++++ 17 files changed, 385 insertions(+), 72 deletions(-) create mode 100644 tests/fixtures/node-test/cases/get-test-context-cancelled.ts create mode 100644 tests/fixtures/node-test/cases/get-test-context.ts diff --git a/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 70143a0c3..25eaebfab 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -6,3 +6,4 @@ - **Timeouts:** when a test's `{ timeout }` elapses, its running subtests are cancelled, as in Node. Subtests do not take their parent's `timeout` as their own, where Node's do, and the subtests of a timed-out test with `{ concurrency }` above 1 run to completion before the test reports. A body that keeps running after its timeout and calls `t.test` again runs that subtest, where Node reports it as not started. - **Thrown values:** a test that throws an object or array that is not an `Error`, such as `throw { code: 42 }`, reports `[object Object]` in the failing section, where Node prints the value as `util.inspect` renders it. - **Test names:** a test without a name, or with an empty one, takes its function's name or ``, as in Node, when the function's name is known at compile time: a function expression, a function declaration, or a `const` bound to an arrow or anonymous function. A name computed at run time, with a function the program received as a parameter, is used as given, so an empty one stays empty where Node uses the function's name. `test(fn)` and `test("", fn)` with such a function throw `SC2020`. +- **Current context:** `getTestContext()` returns the context of the test, suite or hook whose code is running, as in Node: `undefined` at module top level, the root's context in module-level hooks, the suite's in a suite body and in the suite's hooks, and the test's in its body, in the code it awaits, and in the timers, immediates, `process.nextTick` and `queueMicrotask` callbacks it schedules. Node returns, for a suite, a `TestContext` that is a different object from the `SuiteContext` passed to the suite body and its `before` and `after` hooks; in a compiled program they are the same object, so comparing them with `===` gives `true`. Completion callbacks of callback-style I/O APIs, such as `fs.readFile`, get `undefined`, as AsyncLocalStorage stores do. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 0e6387ea9..e2e4cdd19 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -689,9 +689,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 64, + "partial": 65, "refused": 0, - "not-implemented": 53, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -67344,9 +67344,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index a0d7bec4b..0ad30b783 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1162, - "verify-gap": 2166, + "audit-partial": 1163, + "verify-gap": 2165, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -68882,12 +68882,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.getTestContext", + "tests": [ + "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/harness/node-test.test.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index cfbde8940..2f0565f84 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -746,9 +746,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 64, + "partial": 65, "refused": 0, - "not-implemented": 53, + "not-implemented": 52, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -91055,6 +91055,8 @@ "tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-members.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", @@ -94803,8 +94805,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.getTestContext", + "tests": [ + "tests/fixtures/node-test/cases/get-test-context.ts", + "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 2e5eb1c4c..5807c1ae9 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -366,6 +366,7 @@ { "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 3e3f66a2a..0a0837389 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3550,6 +3550,9 @@ declare module "node:test" { [Symbol.dispose](): void; } const mock: MockTracker; + /* The context of the test, suite or hook running in the current async + * context; undefined outside one. */ + function getTestContext(): TestContext | SuiteContext | undefined; /* Declared for typechecking only — every use fences (no lowering). * `assert` is the module-level custom-assertion registry (v24's * `const { assert } = require('node:test')`), not TestContext.assert. */ @@ -3567,6 +3570,7 @@ declare module "node:test" { afterEach, mock, assert, + getTestContext, run, snapshot, TestContext, diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 4780311f8..0b097a1e4 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -49,6 +49,10 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal return finishRuntimeCall(host, e, "scr_test_ctx_passed", a); case "test.workerId": return finishRuntimeCall(host, e, "scr_test_worker_id", a); + case "test.hasContext": + return finishRuntimeCall(host, e, "scr_test_has_context", a); + case "test.context": + return finishRuntimeCall(host, e, "scr_test_context", a); case "test.ctxHook": host.toHeap(args[2]!); return finishRuntimeCall(host, e, "scr_test_ctx_hook", a); diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index c31e674af..d1cc2fcdd 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -25,6 +25,7 @@ import { DYN, F64, STRING, + TESTCTX_T, VOID, isUnitType, } from "../../ir/ir.js"; @@ -652,6 +653,8 @@ export function lowerNodeTestModuleCall( case "beforeEach": case "afterEach": return lowerHook(lowerer, expr, HOOK_WHICH[bi.member]!, `${bi.member}(...)`, loc); + case "getTestContext": + return lowerGetTestContext(lowerer, expr, loc); case "run": lowerer.noLowering( "test.run", @@ -670,13 +673,47 @@ export function lowerNodeTestModuleCall( lowerer.noLowering( `test.${bi.member}`, expr, - "test/it, describe/suite, before/after/beforeEach/afterEach, and the skip/todo/only twins are the lowered node:test members", + "test/it, describe/suite, before/after/beforeEach/afterEach, getTestContext, and the skip/todo/only twins are the lowered node:test members", ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined, ); } return null; // unreachable — noLowering throws } +/** getTestContext(): the context of the test, suite or hook running in + * the current async context, or undefined outside one (module top level, + * and code reached only through timer, immediate, nextTick and + * queueMicrotask callbacks). */ +function lowerGetTestContext(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { + if (expr.arguments.length > 0) { + lowerer.noLowering( + `getTestContext with ${expr.arguments.length} arguments`, + expr, + "the supported form is getTestContext()", + ); + } + const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const missing = declared ? lowerer.wrappedUndefined(declared, loc) : null; + if (!declared || !missing) { + lowerer.noLowering( + "getTestContext() of this type", + expr, + "getTestContext() lowers as TestContext | SuiteContext | undefined", + ); + } + return { + kind: "ternary", + cond: { kind: "libCall", fn: "test.hasContext", args: [], type: BOOL, loc }, + then: lowerer.coerceToExpected( + { kind: "libCall", fn: "test.context", args: [], type: TESTCTX_T, loc }, + declared, + ), + else_: missing, + type: declared, + loc, + }; +} + /** The callable-module form: `test(...)` where the callee identifier IS * the node:test module binding — a default import (`import test from * "node:test"`) or the CJS `const test = require('node:test')` twin @@ -902,7 +939,8 @@ export function lowerTestMethodCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (call.questionDotToken || access.questionDotToken) return null; + if (call.questionDotToken || (access.questionDotToken && !lowerer.chainHandled.has(access))) + return null; const loc = locOf(call); const member = access.name.text; // fn.mock.callCount() and the other mock context members. @@ -1182,7 +1220,9 @@ export function lowerTestCtxProperty( lowerer: Lowerer, expr: ts.PropertyAccessExpression, ): IrExpr | null { - if (expr.questionDotToken) return null; + // `getTestContext()?.name`: the optional-chain step re-enters here with + // the receiver narrowed. + if (expr.questionDotToken && !lowerer.chainHandled.has(expr)) return null; const mockCalls = lowerMockContextProperty(lowerer, expr); if (mockCalls) return mockCalls; if (lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind !== "testCtx") return null; diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 034d76917..df5bf0091 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1479,6 +1479,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ // undefined. "test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL }, "test.workerId": { argTypes: [], result: F64 }, + // getTestContext(): whether a test, suite or hook is running in the + // current async context, and that context. + "test.hasContext": { argTypes: [], result: BOOL }, + "test.context": { argTypes: [], result: TESTCTX_T }, // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 955b0edcc..6e71fe91c 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -25,7 +25,8 @@ export interface RuntimeFeatures { dynInvoke: boolean; dynAsync: boolean; /** The program enters AsyncLocalStorage stores (store.run, enterWith, - * a diagnostics_channel store binding): its timer, immediate, tick and + * a diagnostics_channel store binding) or reads the test runner's + * context (getTestContext): its timer, immediate, tick and * queueMicrotask scheduling goes through the runtime's *_als entry * points, which carry the active stores into each callback. */ asyncContext: boolean; @@ -121,6 +122,9 @@ const ASYNC_CONTEXT_LIB_FNS: ReadonlySet = new Set([ "als.enterWith", "dc.chanBindStore", "dc.chanRunStores", + // getTestContext() reads the test runner's entry in the same snapshot. + "test.hasContext", + "test.context", ]); const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 583b1f6d0..88ad0f7bd 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -665,8 +665,9 @@ struct ScrFiber { /* The ScrAlsCtx layout and the API over it live in scr_runtime.h / * scr_async_dyn.c (gated — the size-class stance); this always-linked - * core keeps only what the fiber machinery itself touches: the active - * slot, the snapshot RC pair, and the switch/spawn/destroy wiring. */ + * core keeps only what the fiber machinery and the test runner touch: the + * active slot, the snapshot RC pair, enter/restore, and the + * switch/spawn/destroy wiring. */ static SCR_TL ScrAlsCtx *scr_als_main_slot = NULL; #ifdef SCR_WORKERS SCR_TL ScrAlsCtx **scr_als_active; @@ -694,6 +695,56 @@ static inline void *scr_fiber_region_capture(void) { return regions != NULL ? regions->capture() : NULL; } +/* Fresh snapshot storage. */ +ScrAlsCtx *scr_als_ctx_alloc(size_t len) { + ScrAlsCtx *c = malloc(sizeof *c + len * sizeof(ScrAlsEntry)); + if (!c) scr_oom(); + c->rc = 1; + c->len = len; + return c; +} + +/* Fresh snapshot with (id → value) replaced-or-appended (value borrowed, + * retained in), installed as the active context; the PREVIOUS snapshot + * returns (ownership moves out) for scr_als_restore. The test runner + * enters its own entry through this too, so it lives in the core. */ +ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) { + ScrAlsCtx *prev = *SCR_ALS_SLOT(); + size_t n = prev ? prev->len : 0; + bool have = false; + for (size_t i = 0; i < n; i++) { + if (prev->entries[i].id == id) { have = true; break; } + } + ScrAlsCtx *next = scr_als_ctx_alloc(have ? n : n + 1); + size_t w = 0; + for (size_t i = 0; i < n; i++) { + if (prev->entries[i].id == id) continue; + next->entries[w].id = prev->entries[i].id; + next->entries[w].value = scr_dyn_retain(prev->entries[i].value); + w++; + } + next->entries[w].id = id; + next->entries[w].value = scr_dyn_retain(value); + *SCR_ALS_SLOT() = next; + return prev; /* ownership moves to the caller */ +} + +void scr_als_restore(ScrAlsCtx *prev) { + scr_als_ctx_release(*SCR_ALS_SLOT()); + *SCR_ALS_SLOT() = prev; /* ownership moves back in */ +} + +/* The value of `id` in the active snapshot (borrowed), or NULL. */ +ScrDyn *scr_als_peek(double id) { + const ScrAlsCtx *c = *SCR_ALS_SLOT(); + if (c) { + for (size_t i = 0; i < c->len; i++) { + if (c->entries[i].id == id) return c->entries[i].value; + } + } + return NULL; +} + /* enterWith on the main stack lasts until that code returns to the loop, * so the store does not reach callbacks scheduled without it: the loop * clears the main slot when the main module's body returns, and an diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h index 96421ca1c..d16fd8b9c 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -160,6 +160,8 @@ ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c); void scr_als_ctx_release(ScrAlsCtx *c); /* After an enterWith: on the main stack, queues the tick that ends it. */ void scr_als_entered_with(void); +ScrAlsCtx *scr_als_ctx_alloc(size_t len); +ScrDyn *scr_als_peek(double id); /* borrowed; NULL when unset */ double scr_als_new(void); ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */ ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */ diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c index 7611378fb..dbddc5b18 100644 --- a/packages/runtime/src/scr_async_dyn.c +++ b/packages/runtime/src/scr_async_dyn.c @@ -28,16 +28,6 @@ static void scr_ad_oom(void) { abort(); } -/* Fresh snapshot storage (the layout lives in scr_runtime.h; the RC pair - * in scr_async.c — spawn/destroy touch it). */ -static ScrAlsCtx *scr_als_ctx_alloc(size_t len) { - ScrAlsCtx *c = malloc(sizeof *c + len * sizeof(ScrAlsEntry)); - if (!c) scr_ad_oom(); - c->rc = 1; - c->len = len; - return c; -} - static SCR_TL double scr_als_counter = 0; /* Main-slot teardown (atexit, LIFO after scr_init's audit registration — @@ -68,30 +58,6 @@ ScrDyn *scr_als_get(double id) { return scr_dyn_retain(scr_dyn_undefined()); } -/* Fresh snapshot with (id → value) replaced-or-appended (value borrowed, - * retained in), installed as the active context; the PREVIOUS snapshot - * returns (ownership moves out) for scr_als_restore. */ -ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) { - ScrAlsCtx *prev = *SCR_ALS_SLOT(); - size_t n = prev ? prev->len : 0; - bool have = false; - for (size_t i = 0; i < n; i++) { - if (prev->entries[i].id == id) { have = true; break; } - } - ScrAlsCtx *next = scr_als_ctx_alloc(have ? n : n + 1); - size_t w = 0; - for (size_t i = 0; i < n; i++) { - if (prev->entries[i].id == id) continue; - next->entries[w].id = prev->entries[i].id; - next->entries[w].value = scr_dyn_retain(prev->entries[i].value); - w++; - } - next->entries[w].id = id; - next->entries[w].value = scr_dyn_retain(value); - *SCR_ALS_SLOT() = next; - return prev; /* ownership moves to the caller */ -} - /* The exit() arm: the id REMOVED from the snapshot. */ ScrAlsCtx *scr_als_enter_absent(double id) { ScrAlsCtx *prev = *SCR_ALS_SLOT(); @@ -112,11 +78,6 @@ ScrAlsCtx *scr_als_enter_absent(double id) { return prev; } -void scr_als_restore(ScrAlsCtx *prev) { - scr_als_ctx_release(*SCR_ALS_SLOT()); - *SCR_ALS_SLOT() = prev; /* ownership moves back in */ -} - void scr_als_enter_with(double id, ScrDyn *value) { ScrAlsCtx *prev = scr_als_enter(id, value); scr_als_ctx_release(prev); /* no restore point — Node's enterWith */ diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 43affdd8a..7c5c243d8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -126,6 +126,7 @@ struct ScrTestCtx { bool passed; /* t.passed: set when the body passes, as Node's pass() does */ bool listed; /* a suite listed in the failing section (a failing root before) */ double start_ms; /* when the test started running; NaN before */ + size_t serial; /* index in scr_test_nodes, getTestContext()'s handle */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -168,6 +169,17 @@ static SCR_TL size_t scr_test_nfibers = 0, scr_test_fibers_cap = 0; /* The module's `mock` tracker: the restore closure of each mock it made. */ static SCR_TL ScrTestHooks scr_test_mocks = {0}; +/* getTestContext(): the test, suite or hook owner whose code is running + * rides the AsyncLocalStorage snapshot under a reserved store id + * (AsyncLocalStorage ids are positive), so it follows a body across its + * awaits and into the async functions it calls, as Node's async context + * does. The entry holds the node's serial, an index in scr_test_nodes + * (borrowed pointers, cleared when a node is freed), so a snapshot that + * outlives its node answers undefined instead of a freed node. */ +#define SCR_TEST_SCOPE_ID (-1.0) +static SCR_TL ScrTestCtx **scr_test_nodes = NULL; +static SCR_TL size_t scr_test_nnodes = 0, scr_test_nodes_cap = 0, scr_test_nodes_live = 0; + #ifdef SCR_RC_AUDIT static SCR_TL long scr_testctx_live = 0; long scr_testctx_live_count(void) { return scr_testctx_live; } @@ -204,6 +216,12 @@ void scr_testctx_release(ScrTestCtx *t) { free(t->children); for (int k = 0; k < 4; k++) scr_test_hooks_free(&t->hooks[k]); scr_test_hooks_free(&t->mocks); + scr_test_nodes[t->serial] = NULL; + if (--scr_test_nodes_live == 0) { + free(scr_test_nodes); + scr_test_nodes = NULL; + scr_test_nnodes = scr_test_nodes_cap = 0; + } #ifdef SCR_RC_AUDIT scr_testctx_live--; #endif @@ -232,12 +250,43 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->timeout = INFINITY; t->concurrency = 1; t->start_ms = NAN; + if (scr_test_nnodes == scr_test_nodes_cap) { + scr_test_nodes_cap = scr_test_nodes_cap ? scr_test_nodes_cap * 2 : 16; + scr_test_nodes = realloc(scr_test_nodes, scr_test_nodes_cap * sizeof *scr_test_nodes); + if (!scr_test_nodes) scr_test_oom(); + } + t->serial = scr_test_nnodes; + scr_test_nodes[scr_test_nnodes++] = t; + scr_test_nodes_live++; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif return t; } +/* Makes `t` the node getTestContext() answers, until the matching + * scr_test_scope_leave. Returns the previous snapshot. */ +static ScrAlsCtx *scr_test_scope_enter(ScrTestCtx *t) { + ScrDyn *serial = scr_dyn_new_num((double)t->serial); + ScrAlsCtx *prev = scr_als_enter(SCR_TEST_SCOPE_ID, serial); + scr_dyn_release(serial); + return prev; +} + +static void scr_test_scope_leave(ScrAlsCtx *prev /*moves*/) { scr_als_restore(prev); } + +/* The node getTestContext() answers (borrowed), or NULL. */ +static ScrTestCtx *scr_test_scope_node(void) { + ScrDyn *serial = scr_als_peek(SCR_TEST_SCOPE_ID); + if (!serial || serial->kind != SCR_DYN_NUM) return NULL; + size_t i = (size_t)serial->v.num; + return i < scr_test_nnodes ? scr_test_nodes[i] : NULL; +} + +bool scr_test_has_context(void) { return scr_test_scope_node() != NULL; } + +ScrTestCtx *scr_test_context(void) { return scr_testctx_retain(scr_test_scope_node()); } + static void scr_test_child_add(ScrTestCtx *parent, ScrTestCtx *child /*moves*/) { if (parent->nchildren == parent->ccap) { parent->ccap = parent->ccap ? parent->ccap * 2 : 4; @@ -393,9 +442,12 @@ static void scr_test_await_timeout(ScrPromise *p, ScrTestCtx *t) { /* Call one stored closure by its lowering-pinned shape; async shapes are * awaited (the runner is a fiber). `ctx` is passed +1 when the body takes - * the TestContext. Returns with the fiber's exception cell holding any - * failure. `timed` is the test whose { timeout } bounds an async body. */ -static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *timed) { + * the TestContext, and `scope` is what getTestContext() answers while it + * runs. Returns with the fiber's exception cell holding any failure. + * `timed` is the test whose { timeout } bounds an async body. */ +static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *scope, + ScrTestCtx *timed) { + ScrAlsCtx *outer = scr_test_scope_enter(scope); bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = @@ -412,6 +464,7 @@ static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); } /* A mock tracker's restoreAll: each mock's restore, in creation order. @@ -434,7 +487,7 @@ static ScrTestHook *scr_test_run_hooks_of(ScrTestCtx *owner, int which, ScrTestC if (scr_exc_pending()) return NULL; /* an earlier hook in the chain threw */ for (size_t i = 0; i < owner->hooks[which].n; i++) { ScrTestHook *h = &owner->hooks[which].v[i]; - scr_test_call(h->cb, h->flags, ctx, NULL); + scr_test_call(h->cb, h->flags, ctx, owner, NULL); if (scr_exc_pending()) return h; /* the hook that threw */ } return NULL; @@ -608,6 +661,7 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { typedef struct { ScrTestCtx *test; /* owned ref */ ScrTestHooks each; /* the afterEach chain, innermost first; owned closures */ + ScrTestCtx **owners; /* owned refs: each afterEach hook's suite or test */ } ScrTestCancelled; static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { @@ -616,11 +670,13 @@ static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { ScrTestCtx *c = job->test; scr_await_hop(); /* the spawn runs the fiber at once; start at the next turn */ for (size_t i = 0; i < job->each.n && !scr_exc_pending(); i++) - scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, NULL); + scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, job->owners[i], NULL); if (scr_exc_pending()) scr_exc_clear(); scr_test_run_hooks_of(c, SCR_HOOK_AFTER, c); if (scr_exc_pending()) scr_exc_clear(); for (int k = 0; k < 4; k++) scr_test_hooks_free(&c->hooks[k]); + for (size_t i = 0; i < job->each.n; i++) scr_testctx_release(job->owners[i]); + free(job->owners); scr_test_hooks_free(&job->each); scr_testctx_release(c); free(job); @@ -636,8 +692,10 @@ static void scr_test_cancelled_hooks(ScrTestCtx *c) { if (job->each.n == job->each.cap) { job->each.cap = job->each.cap ? job->each.cap * 2 : 4; job->each.v = realloc(job->each.v, job->each.cap * sizeof *job->each.v); - if (!job->each.v) scr_test_oom(); + job->owners = realloc(job->owners, job->each.cap * sizeof *job->owners); + if (!job->each.v || !job->owners) scr_test_oom(); } + job->owners[job->each.n] = scr_testctx_retain(p); job->each.v[job->each.n].cb = scr_closure_retain(h->v[i].cb); job->each.v[job->each.n].flags = h->v[i].flags; job->each.v[job->each.n].at = NULL; @@ -775,7 +833,7 @@ static void scr_test_run_node(ScrTestCtx *t) { t->err = scr_exc_take(); failed = true; } else if (t->cb) { - scr_test_call(t->cb, t->flags, t, t); + scr_test_call(t->cb, t->flags, t, t, t); if (t->state != SCR_TS_PENDING) { /* Its parent timed out and cancelled it while the body ran: the * result is reported, its cleanup ran at the cancellation @@ -1703,11 +1761,13 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow if (cb && (int)mode != SCR_TEST_SKIP) { ScrTestCtx *prev = scr_test_parent_get(); scr_test_parent_set(t); + ScrAlsCtx *outer = scr_test_scope_enter(t); /* flag 2: the body takes its SuiteContext (+1, as test bodies do). */ if ((int)flags & SCR_TEST_F_CTX) ((void (*)(ScrClosure *, ScrTestCtx *))cb->fn)(cb, scr_testctx_retain(t)); else ((void (*)(ScrClosure *))cb->fn)(cb); + scr_test_scope_leave(outer); scr_test_parent_set(prev); } scr_closure_release(cb); @@ -1720,6 +1780,7 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow * top-level tests and suites (scr_test_fail_unrun). */ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags) { bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; + ScrAlsCtx *outer = scr_test_scope_enter(root); if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = takes_ctx @@ -1736,6 +1797,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags * unhandled rejection. */ scr_promise_mark_handled(p); scr_test_root_pending[scr_test_root_npending++] = p; + scr_test_scope_leave(outer); return; } scr_promise_release(p); @@ -1744,6 +1806,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); scr_test_root_before_failed(root); } @@ -1822,7 +1885,7 @@ void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { * each descendant subtest, and after runs once the test finishes. */ void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb /*moves*/, double flags) { if ((int)which == SCR_HOOK_BEFORE) { - scr_test_call(cb, (int)flags, t, NULL); + scr_test_call(cb, (int)flags, t, t, NULL); scr_closure_release(cb); if (scr_exc_pending() && !t->before_err) t->before_err = scr_exc_take(); else if (scr_exc_pending()) scr_exc_clear(); @@ -2414,8 +2477,6 @@ ScrStr *scr_test_ctx_name(ScrTestCtx *t) { return t->name ? scr_str_retain(t->name) : scr_str_new("", 0); } -/* t.fullName: the names from the outermost suite or test down to this one, - * joined with " > ". */ bool scr_test_ctx_passed(ScrTestCtx *t) { return t->passed; } /* t.workerId: Number(process.env.NODE_TEST_WORKER_ID) || undefined, with 0 @@ -2429,7 +2490,10 @@ double scr_test_worker_id(void) { return isnan(n) ? 0 : n; } +/* t.fullName: the names from the outermost suite or test down to this one, + * joined with " > "; the root's own name for the root. */ ScrStr *scr_test_ctx_full_name(ScrTestCtx *t) { + if (!t->parent) return scr_test_ctx_name(t); const ScrTestCtx *chain[64]; int n = 0; for (const ScrTestCtx *p = t; p && p->parent && n < 64; p = p->parent) chain[n++] = p; diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index b29fca362..faa46406f 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -52,6 +52,10 @@ ScrStr *scr_test_ctx_full_name(ScrTestCtx *t); bool scr_test_ctx_passed(ScrTestCtx *t); double scr_test_worker_id(void); ScrStr *scr_test_ctx_name(ScrTestCtx *t); /* +1 */ +/* getTestContext(): whether a test, suite or hook is running in the + * current async context, and its context (+1; call when has_context). */ +bool scr_test_has_context(void); +ScrTestCtx *scr_test_context(void); /* Mock trackers: the module's `mock` and each test's t.mock. track keeps * the closure that restores one mock; restore_all calls them in creation * order; reset calls them and forgets them. A test's t.mock is reset when diff --git a/tests/fixtures/node-test/cases/get-test-context-cancelled.ts b/tests/fixtures/node-test/cases/get-test-context-cancelled.ts new file mode 100644 index 000000000..3317a2914 --- /dev/null +++ b/tests/fixtures/node-test/cases/get-test-context-cancelled.ts @@ -0,0 +1,40 @@ +// @exit: 1 +// getTestContext() in the cleanup hooks of a subtest cancelled by its +// parent's timeout answers each hook's owner, as Node does: the suite for +// its afterEach, the parent test for its t.afterEach, and the cancelled +// subtest for its own t.after. +import { afterEach, describe, getTestContext, it, test } from "node:test"; +import * as assert from "node:assert"; + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const seen: string[] = []; + +function record(where: string): void { + seen.push(`${where}: ${getTestContext()?.fullName ?? "undefined"}`); +} + +describe("suite", () => { + afterEach((c) => record(`suite afterEach for ${c.name}`)); + + it("parent", { timeout: 30 }, async (t) => { + t.afterEach((c) => record(`parent afterEach for ${c.name}`)); + await t.test("sub", async (u) => { + u.after(() => record("sub after")); + record("sub body"); + await sleep(200); + record("sub settles"); + }); + }); +}); + +test("next", async () => { + await sleep(300); + assert.deepStrictEqual(seen, [ + "sub body: suite > parent > sub", + "suite afterEach for parent: suite", + "parent afterEach for sub: suite > parent", + "suite afterEach for sub: suite", + "sub after: suite > parent > sub", + "sub settles: suite > parent > sub", + ]); +}); diff --git a/tests/fixtures/node-test/cases/get-test-context.ts b/tests/fixtures/node-test/cases/get-test-context.ts new file mode 100644 index 000000000..2b61841cc --- /dev/null +++ b/tests/fixtures/node-test/cases/get-test-context.ts @@ -0,0 +1,122 @@ +// getTestContext() answers the context of the test, suite or hook whose +// code is running: undefined at module top level, "" in module-level +// hooks, the suite in its body, the hook's owner in a hook (a suite's +// beforeEach answers the suite), and the test in its body, across awaits, +// in async functions, promise callbacks and timers it starts, and in +// subtests. A timer keeps its test's context after the test ends. +// Concurrent subtests each answer their own context across their awaits. +import { after, afterEach, before, beforeEach, describe, getTestContext, it, test } from "node:test"; +import * as assert from "node:assert"; + +const seen: string[] = []; +let late = ""; + +function record(where: string): void { + const c = getTestContext(); + seen.push(`${where}: ${c === undefined ? "undefined" : c.fullName}`); +} + +async function helper(where: string): Promise { + await Promise.resolve(); + record(where); +} + +const tick = (): Promise => new Promise((resolve) => setTimeout(resolve, 2)); + +record("top level"); + +before(() => record("root before")); +beforeEach(() => record("root beforeEach")); + +describe("suite", () => { + record("suite body"); + before(() => record("suite before")); + beforeEach(() => record("suite beforeEach")); + afterEach(() => record("suite afterEach")); + after(() => record("suite after")); + + it("first", async () => { + record("first body"); + await tick(); + record("first after await"); + await helper("first helper"); + await Promise.resolve().then(() => record("first then")); + await new Promise((resolve) => + setTimeout(() => { + record("first timer"); + resolve(); + }, 1), + ); + }); + + describe("inner", () => { + record("inner body"); + it("second", () => record("second body")); + }); +}); + +test("subtests", async (t) => { + t.beforeEach(() => record("subtests beforeEach")); + await t.test("sub", async () => { + record("sub body"); + await tick(); + record("sub after await"); + }); + record("subtests after sub"); + const c = getTestContext(); + assert.strictEqual(c === t, true); + setTimeout(() => { + late = getTestContext()?.name ?? "undefined"; + }, 0); +}); + +test("concurrent", { concurrency: true }, async (t) => { + const order: string[] = []; + const body = async (): Promise => { + order.push(getTestContext()?.name ?? "undefined"); + await tick(); + order.push(getTestContext()?.name ?? "undefined"); + await tick(); + order.push(getTestContext()?.name ?? "undefined"); + }; + t.after(() => { + seen.push(`concurrent: ${order.join(" ")}`); + }); + t.test("c1", body); + t.test("c2", body); +}); + +test("check", () => { + assert.strictEqual(late, "subtests"); + assert.deepStrictEqual(seen, [ + "top level: undefined", + "root before: ", + "suite body: suite", + "inner body: suite > inner", + "suite before: suite", + "root beforeEach: ", + "suite beforeEach: suite", + "first body: suite > first", + "first after await: suite > first", + "first helper: suite > first", + "first then: suite > first", + "first timer: suite > first", + "suite afterEach: suite", + "root beforeEach: ", + "suite beforeEach: suite", + "second body: suite > inner > second", + "suite afterEach: suite", + "suite after: suite", + "root beforeEach: ", + "root beforeEach: ", + "subtests beforeEach: subtests", + "sub body: subtests > sub", + "sub after await: subtests > sub", + "subtests after sub: subtests", + "root beforeEach: ", + "root beforeEach: ", + "root beforeEach: ", + "concurrent: c1 c2 c1 c2 c1 c2", + "root beforeEach: ", + ]); +}); From 3c749a2895f1d86d8e4215a7a5a50e39a589aa29 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 06:33:33 -0500 Subject: [PATCH 67/67] Read AST lists through their nodes The frontend's node lists become node lists, { pos, end, nodes }, the shape of tsc-ts's NodeList, so that the frontend can later read tsc-ts's own nodes. AstNode's list getters return an AstNodeList, ts.NodeArray is that class, and the frontend reads a list's elements through nodes. The lists already carried their positions on the wire. The class is the type, not a structural interface, because native code represents a class and a record differently. A decoded file keeps each list's nodes array, as before, and each read of a list field returns a new AstNodeList over it, so the file holds no more objects than before. nodes keeps its identity; a list object is not the same object on each read. The change is mechanical: a type-aware rewrite of each list field read, plus hand edits where lists are read through casts, generic accessors or test mocks. The generated getters and types follow from generate-ts7-ast-schema.mjs. AstFile and Ts7Session drop their unused listMetadata hook. --- packages/compiler/src/frontend/cjs-syntax.ts | 62 +- .../compiler/src/frontend/contract-types.ts | 8 +- packages/compiler/src/frontend/fork-target.ts | 20 +- .../src/frontend/function-completion.ts | 12 +- .../src/frontend/inferred-js-diagnostics.ts | 11 +- .../compiler/src/frontend/lib-contract.ts | 48 +- packages/compiler/src/frontend/lib-exports.ts | 4 +- .../src/frontend/lowering/assert-instances.ts | 4 +- .../src/frontend/lowering/builtin-calls.ts | 2 +- .../frontend/lowering/builtins/arguments.ts | 2 +- .../lowering/builtins/async-context.ts | 103 +-- .../src/frontend/lowering/builtins/atomics.ts | 12 +- .../src/frontend/lowering/builtins/blob.ts | 6 +- .../lowering/builtins/callable-aliases.ts | 2 +- .../lowering/builtins/child-process-async.ts | 14 +- .../builtins/child-process-methods.ts | 96 +-- .../lowering/builtins/child-process-sync.ts | 20 +- .../lowering/builtins/child-process.ts | 96 +-- .../frontend/lowering/builtins/compression.ts | 73 +- .../frontend/lowering/builtins/constants.ts | 2 +- .../src/frontend/lowering/builtins/crypto.ts | 68 +- .../src/frontend/lowering/builtins/dates.ts | 28 +- .../lowering/builtins/disposable-stack.ts | 6 +- .../frontend/lowering/builtins/environment.ts | 6 +- .../src/frontend/lowering/builtins/errors.ts | 4 +- .../lowering/builtins/filesystem-calls.ts | 233 +++--- .../frontend/lowering/builtins/filesystem.ts | 154 ++-- .../src/frontend/lowering/builtins/fs-glob.ts | 6 +- .../frontend/lowering/builtins/fs-streams.ts | 4 +- .../src/frontend/lowering/builtins/ipc.ts | 14 +- .../src/frontend/lowering/builtins/json.ts | 32 +- .../frontend/lowering/builtins/key-object.ts | 4 +- .../lowering/builtins/module-bindings.ts | 12 +- .../lowering/builtins/module-calls.ts | 92 +-- .../lowering/builtins/module-resolution.ts | 32 +- .../frontend/lowering/builtins/net-address.ts | 8 +- .../src/frontend/lowering/builtins/numbers.ts | 18 +- .../lowering/builtins/operating-system.ts | 24 +- .../frontend/lowering/builtins/performance.ts | 15 +- .../src/frontend/lowering/builtins/process.ts | 186 ++--- .../frontend/lowering/builtins/promises.ts | 50 +- .../frontend/lowering/builtins/querystring.ts | 28 +- .../frontend/lowering/builtins/readline.ts | 39 +- .../frontend/lowering/builtins/reflection.ts | 30 +- .../src/frontend/lowering/builtins/strings.ts | 25 +- .../frontend/lowering/builtins/text-codecs.ts | 63 +- .../src/frontend/lowering/builtins/timers.ts | 26 +- .../src/frontend/lowering/builtins/url.ts | 28 +- .../frontend/lowering/builtins/util-types.ts | 12 +- .../src/frontend/lowering/builtins/workers.ts | 20 +- .../src/frontend/lowering/call-arguments.ts | 8 +- .../src/frontend/lowering/call-signatures.ts | 33 +- .../frontend/lowering/cjs-exports-object.ts | 8 +- .../frontend/lowering/cjs-inline-require.ts | 6 +- .../src/frontend/lowering/cjs-tsc-interop.ts | 12 +- .../frontend/lowering/class-descriptors.ts | 14 +- .../lowering/class-instance-constructor.ts | 2 +- .../lowering/class-method-mutations.ts | 34 +- .../frontend/lowering/class-method-values.ts | 2 +- .../lowering/class-protocol-dispatch.ts | 13 +- .../src/frontend/lowering/class-protocols.ts | 20 +- .../src/frontend/lowering/class-prototypes.ts | 26 +- .../lowering/class-runtime-statics.ts | 2 +- .../frontend/lowering/class-static-data.ts | 2 +- .../lowering/coercions/structural-plans.ts | 2 +- .../frontend/lowering/collection-ingestion.ts | 2 +- .../lowering/construction-escapes.test.ts | 6 +- .../frontend/lowering/construction-escapes.ts | 9 +- .../containers/array-callback-call.ts | 2 +- .../lowering/containers/array-callbacks.ts | 36 +- .../lowering/containers/array-concat.ts | 2 +- .../lowering/containers/array-construction.ts | 20 +- .../lowering/containers/array-from-async.ts | 2 +- .../lowering/containers/array-methods.ts | 180 +++-- .../src/frontend/lowering/containers/bytes.ts | 147 ++-- .../lowering/containers/callback-arguments.ts | 8 +- .../containers/collection-grouping.ts | 17 +- .../lowering/containers/collection-methods.ts | 42 +- .../lowering/containers/exec-result-uses.ts | 2 +- .../frontend/lowering/containers/for-of.ts | 36 +- .../lowering/containers/indexed-objects.ts | 29 +- .../lowering/containers/iterator-helpers.ts | 81 +- .../lowering/containers/match-row-uses.ts | 29 +- .../lowering/containers/string-and-regexp.ts | 51 +- .../lowering/containers/string-replacement.ts | 2 +- .../lowering/deferred-module-initializers.ts | 33 +- .../src/frontend/lowering/devirtualize.ts | 4 +- .../src/frontend/lowering/divergences.ts | 52 +- .../lowering/expressions/native-proxy.ts | 6 +- .../lowering/expressions/object-literals.ts | 114 +-- .../lowering/expressions/optional-chains.ts | 4 +- .../src/frontend/lowering/function-returns.ts | 10 +- .../frontend/lowering/generic-functions.ts | 95 +-- .../src/frontend/lowering/import-meta.ts | 2 +- .../src/frontend/lowering/intern-tables.ts | 19 +- .../src/frontend/lowering/js-class-fields.ts | 14 +- .../frontend/lowering/js-dynamic-objects.ts | 34 +- .../src/frontend/lowering/lifetime-regions.ts | 17 +- .../src/frontend/lowering/lower-assert.ts | 114 +-- .../lowering/lower-async-iteration.ts | 10 +- .../src/frontend/lowering/lower-calls.ts | 689 +++++++++--------- .../src/frontend/lowering/lower-classes.ts | 445 +++++------ .../src/frontend/lowering/lower-comptime.ts | 6 +- .../src/frontend/lowering/lower-console.ts | 24 +- .../src/frontend/lowering/lower-dgram.ts | 10 +- .../src/frontend/lowering/lower-enums.ts | 6 +- .../frontend/lowering/lower-event-emitter.ts | 56 +- .../frontend/lowering/lower-event-target.ts | 4 +- .../src/frontend/lowering/lower-expando.ts | 2 +- .../src/frontend/lowering/lower-exprs.ts | 136 ++-- .../src/frontend/lowering/lower-functions.ts | 20 +- .../src/frontend/lowering/lower-generators.ts | 6 +- .../src/frontend/lowering/lower-inspect.ts | 58 +- .../src/frontend/lowering/lower-mixins.ts | 30 +- .../src/frontend/lowering/lower-mock.ts | 22 +- .../src/frontend/lowering/lower-modules.ts | 81 +- .../src/frontend/lowering/lower-namespaces.ts | 6 +- .../frontend/lowering/lower-node-module.ts | 4 +- .../src/frontend/lowering/lower-server.ts | 116 +-- .../src/frontend/lowering/lower-stdlib.ts | 139 ++-- .../src/frontend/lowering/lower-stmts.ts | 170 ++--- .../src/frontend/lowering/lower-stream.ts | 56 +- .../src/frontend/lowering/lower-test.ts | 24 +- .../src/frontend/lowering/lower-timers.ts | 66 +- .../src/frontend/lowering/lower-url.ts | 2 +- .../compiler/src/frontend/lowering/lowerer.ts | 150 ++-- .../src/frontend/lowering/nan-coded-reads.ts | 36 +- .../frontend/lowering/narrowing-stability.ts | 34 +- .../src/frontend/lowering/native-ffi.ts | 10 +- .../frontend/lowering/object-factory-new.ts | 8 +- .../lowering/require-initialization.ts | 6 +- .../lowering/runtime-optional-bounds.ts | 8 +- .../lowering/runtime-optional-elements.ts | 22 +- .../lowering/runtime-optional-guards.test.ts | 2 +- .../lowering/runtime-optional-guards.ts | 18 +- .../lowering/runtime-optional-ownership.ts | 14 +- .../lowering/runtime-optional-records.ts | 42 +- .../src/frontend/lowering/split-consumers.ts | 4 +- .../frontend/lowering/string-index-bounds.ts | 4 +- .../src/frontend/lowering/surfaces.ts | 4 +- .../src/frontend/lowering/symbol-fields.ts | 27 +- .../src/frontend/lowering/symbol-methods.ts | 2 +- .../src/frontend/lowering/thread-slots.ts | 13 +- .../src/frontend/lowering/var-hoisting.ts | 8 +- .../src/frontend/module-source-candidates.ts | 2 +- .../compiler/src/frontend/module-syntax.ts | 18 +- .../src/frontend/npm-static-bundled-cjs.ts | 32 +- .../frontend/npm-static-declaration-syntax.ts | 154 ++-- .../compiler/src/frontend/npm-static-prune.ts | 18 +- .../src/frontend/npm-static-rewrite-syntax.ts | 82 +-- .../src/frontend/npm-static-subtyping.ts | 6 +- .../compiler/src/frontend/pipeline.test.ts | 2 +- packages/compiler/src/frontend/pipeline.ts | 4 +- .../compiler/src/frontend/program.test.ts | 4 +- packages/compiler/src/frontend/program.ts | 231 +++--- .../compiler/src/frontend/services.test.ts | 2 +- .../compiler/src/frontend/tier-analysis.ts | 4 +- .../src/frontend/ts7/ast-node.test.ts | 30 +- .../compiler/src/frontend/ts7/ast-node.ts | 81 +- .../src/frontend/ts7/ast-types.generated.ts | 2 +- .../compiler/src/frontend/ts7/ast-types.ts | 6 +- packages/compiler/src/frontend/ts7/ast.ts | 4 +- .../compiler/src/frontend/ts7/census-check.ts | 2 +- .../compiler/src/frontend/ts7/checker.test.ts | 4 +- packages/compiler/src/frontend/ts7/checker.ts | 2 +- .../src/frontend/ts7/semantic-checker.test.ts | 8 +- .../compiler/src/frontend/ts7/session.test.ts | 2 +- packages/compiler/src/frontend/ts7/session.ts | 3 +- .../src/frontend/ts7/source-parser.test.ts | 32 +- packages/compiler/src/frontend/type-mapper.ts | 27 +- .../src/frontend/type-only-imports.ts | 8 +- .../compiler/src/frontend/worker-target.ts | 10 +- packages/compiler/src/ir/traverse.test.ts | 38 +- packages/compiler/src/wasm/discover.ts | 10 +- packages/compiler/test/ts7/facade.test.ts | 46 +- .../compiler/test/ts7/program-adapter.test.ts | 2 +- packages/compiler/test/ts7/rpc-api.test.ts | 42 +- scripts/generate-ts7-ast-schema.mjs | 6 +- scripts/generate-ts7-ast-views.mjs | 2 + .../self-hosting/frontend-services-cases.ts | 4 +- tests/fixtures/self-hosting/ts7-ast-views.ts | 22 +- .../self-hosting/ts7-checker-cases.ts | 12 +- .../self-hosting/ts7-checker-snapshots.ts | 2 +- .../fixtures/self-hosting/ts7-client-cases.ts | 24 +- .../self-hosting/ts7-program-host-cases.ts | 4 +- .../self-hosting/ts7-semantic-cases.ts | 16 +- .../self-hosting/ts7-source-parser-cases.ts | 4 +- tests/harness/stdlib-surface.test.ts | 4 +- 188 files changed, 3624 insertions(+), 3263 deletions(-) diff --git a/packages/compiler/src/frontend/cjs-syntax.ts b/packages/compiler/src/frontend/cjs-syntax.ts index 8edd52b09..41191432a 100644 --- a/packages/compiler/src/frontend/cjs-syntax.ts +++ b/packages/compiler/src/frontend/cjs-syntax.ts @@ -56,8 +56,8 @@ function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string { export function bareRequireSpecOf(e: ts.Node): string | null { if (!ts.isCallExpression(e) || e.questionDotToken !== undefined) return null; if (!ts.isIdentifier(e.expression) || e.expression.text !== "require") return null; - if (e.arguments.length !== 1) return null; - const arg = e.arguments[0]!; + if (e.arguments.nodes.length !== 1) return null; + const arg = e.arguments.nodes[0]!; return ts.isStringLiteral(arg) ? arg.text : null; } @@ -135,7 +135,7 @@ function scanTableLiteral( reexports: string[], ): void { const text = sf.text; - for (const prop of obj.properties) { + for (const prop of obj.properties.nodes) { if (ts.isGetAccessor(prop)) { // `get name() {...}` — merve's explicit early termination... unless // the accessor's NAME is not a plain identifier, where the lexer @@ -253,8 +253,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set ) { return; } - if (call.arguments.length !== 3) return; - const [recv, nameArg, desc] = call.arguments as unknown as [ + if (call.arguments.nodes.length !== 3) return; + const [recv, nameArg, desc] = call.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -263,7 +263,7 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set if (!ts.isStringLiteral(nameArg)) return; if (!ts.isObjectLiteralExpression(desc)) return; let i = 0; - const props = desc.properties; + const props = desc.properties.nodes; const first = props[0]; if ( first !== undefined && @@ -287,8 +287,8 @@ function scanDefineProperty(call: ts.CallExpression, sf: ts.SourceFile, out: Set // and the getter must be the descriptor's LAST property. const body = getterBodyOf(p); if (body === undefined || i !== props.length - 1) return; - if (body.statements.length !== 1) return; - const ret = body.statements[0]!; + if (body.statements.nodes.length !== 1) return; + const ret = body.statements.nodes[0]!; if (!ts.isReturnStatement(ret) || ret.expression === undefined) return; const r = ret.expression; const returnsIdentish = @@ -313,7 +313,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined { p.name.text === "get" && p.asteriskToken === undefined && (ts.getModifiers(p) ?? []).length === 0 && - p.parameters.length === 0 + p.parameters.nodes.length === 0 ) { return p.body; } @@ -322,7 +322,7 @@ function getterBodyOf(p: ts.ObjectLiteralElementLike): ts.Block | undefined { ts.isIdentifier(p.name) && p.name.text === "get" && ts.isFunctionExpression(p.initializer) && - p.initializer.parameters.length === 0 && + p.initializer.parameters.nodes.length === 0 && p.initializer.asteriskToken === undefined && (ts.getModifiers(p.initializer) ?? []).length === 0 ) { @@ -349,7 +349,7 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string | if (nameNode === undefined) return null; if (nameNode.text !== "__export" && nameNode.text !== "__exportStar") return null; if (!sourceSpellsIdentifier(nameNode, sf)) return null; - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (arg0 === undefined) return null; if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null; return leadingRequireOf(arg0, sf)?.spec ?? null; @@ -369,7 +369,7 @@ function byteAdjacentParen(sf: ts.SourceFile, nameEnd: number, argStart: number) * probed: a member-qualified helper or one space after its paren misses). * Answers [ID, spec]. */ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, string] | null { - const decl = stmt.declarationList.declarations[0]; + const decl = stmt.declarationList.declarations.nodes[0]; if (decl === undefined || !ts.isIdentifier(decl.name) || !sourceSpellsIdentifier(decl.name, sf)) return null; const init = decl.initializer; @@ -381,9 +381,9 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s init.questionDotToken === undefined && ts.isIdentifier(init.expression) && init.expression.text === "_interopRequireWildcard" && - init.arguments.length >= 1 + init.arguments.nodes.length >= 1 ) { - const arg0 = init.arguments[0]!; + const arg0 = init.arguments.nodes[0]!; if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null; const spec = bareRequireSpecOf(arg0); if (spec !== null) return [decl.name.text, spec]; @@ -434,8 +434,8 @@ function isHasOwnCall(e: ts.Expression, key: string): boolean { ) cur = cur.expression; if (!ts.isIdentifier(cur) || cur.text !== "Object") return false; - if (e.arguments.length !== 2) return false; - const [recv, k] = e.arguments as unknown as [ts.Expression, ts.Expression]; + if (e.arguments.nodes.length !== 2) return false; + const [recv, k] = e.arguments.nodes as unknown as [ts.Expression, ts.Expression]; return ts.isIdentifier(recv) && ts.isIdentifier(k) && k.text === key; } @@ -448,9 +448,9 @@ function isDirectHasOwnCall(e: ts.Expression, key: string): boolean { ts.isIdentifier(e.expression.name) && e.expression.name.text === "hasOwnProperty" && ts.isIdentifier(e.expression.expression) && - e.arguments.length === 1 && - ts.isIdentifier(e.arguments[0]!) && - (e.arguments[0] as ts.Identifier).text === key + e.arguments.nodes.length === 1 && + ts.isIdentifier(e.arguments.nodes[0]!) && + (e.arguments.nodes[0] as ts.Identifier).text === key ); } @@ -477,16 +477,16 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean ) { return false; } - if (e.arguments.length !== 3) return false; - const [recv, nameArg, desc] = e.arguments as unknown as [ + if (e.arguments.nodes.length !== 3) return false; + const [recv, nameArg, desc] = e.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, ]; if (!isExportsTarget(recv)) return false; if (!ts.isIdentifier(nameArg) || nameArg.text !== key) return false; - if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 2) return false; - const [en, get] = desc.properties as unknown as [ + if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 2) return false; + const [en, get] = desc.properties.nodes as unknown as [ ts.ObjectLiteralElementLike, ts.ObjectLiteralElementLike, ]; @@ -494,8 +494,8 @@ function isCopyStatement(stmt: ts.Statement, id1: string, key: string): boolean return false; if (en.initializer.kind !== ts.SyntaxKind.TrueKeyword) return false; const body = getterBodyOf(get); - if (body === undefined || body.statements.length !== 1) return false; - const ret = body.statements[0]!; + if (body === undefined || body.statements.nodes.length !== 1) return false; + const ret = body.statements.nodes[0]!; if (!ts.isReturnStatement(ret) || ret.expression === undefined) return false; return isKeyedAccess(ret.expression, (x) => ts.isIdentifier(x) && x.text === id1, key); } @@ -541,16 +541,16 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null ) { return null; } - if (keysCall.arguments.length !== 1) return null; - const id1Node = keysCall.arguments[0]!; + if (keysCall.arguments.nodes.length !== 1) return null; + const id1Node = keysCall.arguments.nodes[0]!; if (!ts.isIdentifier(id1Node) || !sourceSpellsIdentifier(id1Node, sf)) return null; const id1 = id1Node.text; - const fn = call.arguments[0]; + const fn = call.arguments.nodes[0]; if (fn === undefined || !ts.isFunctionExpression(fn)) return null; // arrows never match (probed) if (fn.name !== undefined) return null; // a NAMED callback misses (probed) if (fn.asteriskToken !== undefined || (ts.getModifiers(fn) ?? []).length > 0) return null; - if (fn.parameters.length !== 1) return null; - const keyParam = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const keyParam = fn.parameters.nodes[0]!; if ( !ts.isIdentifier(keyParam.name) || keyParam.initializer !== undefined || @@ -558,7 +558,7 @@ function starLoopIdOf(call: ts.CallExpression, sf: ts.SourceFile): string | null ) return null; const key = keyParam.name.text; - const stmts = fn.body.statements; + const stmts = fn.body.statements.nodes; // Form B: the single `if (KEY !== 'default' [&& !hasOwn]) COPY` statement. if (stmts.length === 1 && ts.isIfStatement(stmts[0]!) && stmts[0]!.elseStatement === undefined) { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index 24462a2ce..9ba041d2c 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -262,7 +262,7 @@ function placeHeritage( return unsupported(out, node, marker); // `implements Acyclic` would make the class implement C's own type, // which the lowering treats as a protocol of C. - if ((node.typeArguments?.length ?? 0) > 0) return unsupported(out, node, marker); + if ((node.typeArguments?.nodes.length ?? 0) > 0) return unsupported(out, node, marker); const note = (name: string): ContractNote => ({ marker, loc: locOf(node), name }); if ( (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && @@ -324,8 +324,8 @@ function place( ts.isTypeReferenceNode(parent) && ts.isIdentifier(parent.typeName) && (parent.typeName.text === "Array" || parent.typeName.text === "ReadonlyArray") && - parent.typeArguments?.length === 1 && - parent.typeArguments[0] === ref + parent.typeArguments?.nodes.length === 1 && + parent.typeArguments.nodes[0] === ref ) { node = parent; parent = parent.parent; @@ -336,7 +336,7 @@ function place( // contract of the function the NoThrow annotates. if (marker !== "NoThrow" && parent !== undefined && ts.isTypeReferenceNode(parent)) { const wrapper = markerOf(checker, parent); - if (wrapper === "NoThrow" && parent.typeArguments?.[0] === node) { + if (wrapper === "NoThrow" && parent.typeArguments?.nodes[0] === node) { node = parent; parent = parent.parent; } diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 0562467be..a7248c674 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -158,13 +158,13 @@ function staticBoolean( if ( ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isPropertyAccessExpression(current.expression) && !current.expression.questionDotToken && (current.expression.name.text === "endsWith" || current.expression.name.text === "startsWith") ) { const receiver = staticString(program, current.expression.expression, seen); - const needle = staticString(program, current.arguments[0]!, seen); + const needle = staticString(program, current.arguments.nodes[0]!, seen); if (receiver === null || needle === null) return null; return current.expression.name.text === "endsWith" ? receiver.endsWith(needle) @@ -194,7 +194,7 @@ function staticString( if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href; if (ts.isTemplateExpression(current)) { let value = current.head.text; - for (const span of current.templateSpans) { + for (const span of current.templateSpans.nodes) { const part = staticString(program, span.expression, seen); if (part === null) return null; value += part + span.literal.text; @@ -219,11 +219,11 @@ function staticString( if ( ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isIdentifier(current.expression) && isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath") ) { - const href = staticString(program, current.arguments[0]!, seen); + const href = staticString(program, current.arguments.nodes[0]!, seen); if (href === null) return null; try { return fileURLToPath(href); @@ -237,7 +237,7 @@ function staticString( (isBuiltinMemberImport(program, current.expression, "url", "URL") || (current.expression.text === "URL" && isNodeGlobal(program, current.expression))) ) { - const args = current.arguments ?? []; + const args = current.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2) return null; const input = staticString(program, args[0]!, seen); const base = args[1] === undefined ? null : staticString(program, args[1], seen); @@ -261,7 +261,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression): const supported = (ts.isCallExpression(current) && !current.questionDotToken && - current.arguments.length === 1 && + current.arguments.nodes.length === 1 && ts.isIdentifier(current.expression) && isBuiltinMemberImport(program, current.expression, "url", "fileURLToPath")) || (ts.isNewExpression(current) && @@ -279,7 +279,7 @@ export function staticForkModulePath(program: ts.Program, expr: ts.Expression): } export function forkCallModulePath(program: ts.Program, call: ts.CallExpression): string | null { - if (call.questionDotToken || call.arguments[0] === undefined) return null; + if (call.questionDotToken || call.arguments.nodes[0] === undefined) return null; const callee = strip(call.expression); const isFork = (ts.isIdentifier(callee) && isBuiltinMemberImport(program, callee, "child_process", "fork")) || @@ -291,7 +291,7 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression) if (!isFork) { return null; } - return staticForkModulePath(program, call.arguments[0]); + return staticForkModulePath(program, call.arguments.nodes[0]); } /** Source-order static fork targets in the supplied files, deduplicated by @@ -304,7 +304,7 @@ export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFi const calls = moduleSourceCandidates(program, sourceFile).calls; const bindings: ts.Node[] = []; for (const call of calls) { - if (call.questionDotToken || call.arguments[0] === undefined) continue; + if (call.questionDotToken || call.arguments.nodes[0] === undefined) continue; const callee = strip(call.expression); if (ts.isIdentifier(callee)) bindings.push(callee); else if ( diff --git a/packages/compiler/src/frontend/function-completion.ts b/packages/compiler/src/frontend/function-completion.ts index 7c8d8a9fc..02c85b729 100644 --- a/packages/compiler/src/frontend/function-completion.ts +++ b/packages/compiler/src/frontend/function-completion.ts @@ -6,14 +6,14 @@ export function functionCanFallThrough(declaration: ts.Node): boolean { if ( !ts.isFunctionLike(declaration) || declaration.asteriskToken || - declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const body = declaration.body as ts.Block | ts.Expression | undefined; if (!body || !ts.isBlock(body)) return false; const completes = (statement: ts.Statement): boolean => { if (ts.isReturnStatement(statement) || ts.isThrowStatement(statement)) return false; - if (ts.isBlock(statement)) return sequence(statement.statements); + if (ts.isBlock(statement)) return sequence(statement.statements.nodes); if (ts.isIfStatement(statement)) return ( completes(statement.thenStatement) || @@ -29,13 +29,13 @@ export function functionCanFallThrough(declaration: ts.Node): boolean { } if (ts.isSwitchStatement(statement)) return ( - !statement.caseBlock.clauses.some(ts.isDefaultClause) || - statement.caseBlock.clauses.some((clause) => sequence(clause.statements)) + !statement.caseBlock.clauses.nodes.some(ts.isDefaultClause) || + statement.caseBlock.clauses.nodes.some((clause) => sequence(clause.statements.nodes)) ); return true; }; const sequence = (statements: readonly ts.Statement[]): boolean => statements.every(completes); - return sequence(body.statements); + return sequence(body.statements.nodes); } /** A documented JavaScript result must also represent explicit empty returns. */ @@ -44,7 +44,7 @@ export function functionCanReturnUndefined(declaration: ts.Node): boolean { if ( !ts.isFunctionLike(declaration) || declaration.asteriskToken || - declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const body = declaration.body; diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index 0fe38c258..a4ad48892 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -50,9 +50,10 @@ export function inferredJsDiagnosticSuppressed( (ts.isFunctionDeclaration(declaration) || ts.isFunctionExpression(declaration) || ts.isMethodDeclaration(declaration)) && - node.arguments.length > declaration.parameters.length && - node.arguments[declaration.parameters.length]?.getStart(file) === diagnostic.pos && - node.arguments[node.arguments.length - 1]?.getEnd() === diagnostic.end && + node.arguments.nodes.length > declaration.parameters.nodes.length && + node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) === + diagnostic.pos && + node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end && bodyReadsArguments(declaration) ) { suppressed = true; @@ -92,8 +93,8 @@ export function inferredJsDiagnosticSuppressed( ts.isMethodDeclaration(declaration) && declaration.type === undefined && isJsSourceFileName(declaration.getSourceFile().fileName) && - declaration.body?.statements.length === 1 && - ts.isThrowStatement(declaration.body.statements[0]!) + declaration.body?.statements?.nodes.length === 1 && + ts.isThrowStatement(declaration.body.statements.nodes[0]!) ) { suppressed = true; return "stop"; diff --git a/packages/compiler/src/frontend/lib-contract.ts b/packages/compiler/src/frontend/lib-contract.ts index 71217ecda..0432d06e2 100644 --- a/packages/compiler/src/frontend/lib-contract.ts +++ b/packages/compiler/src/frontend/lib-contract.ts @@ -141,7 +141,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set { if (name !== undefined && CONTRACT_GLOBAL_TYPES.has(name.text)) names.add(name.text); }; - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if ( ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt) || @@ -160,7 +160,7 @@ function localTypeBindings(file: ts.SourceFile, bindings: TypeBindings): Set ts.isTypeParameterDeclaration(p) && p.name.text === name) === true) return false; } @@ -254,7 +254,7 @@ function typeShape( return { k: "array", elem: typeShape(file, node.elementType, bindings) }; if (ts.isTupleTypeNode(node)) { const elems: ContractTypeShape[] = []; - for (const e of node.elements) { + for (const e of node.elements.nodes) { // Named tuple members carry the type under `.type`. const t = ts.isNamedTupleMember(e) ? e.type : (e as ts.TypeNode); elems.push(typeShape(file, t, bindings)); @@ -262,13 +262,13 @@ function typeShape( return { k: "tuple", elems }; } if (ts.isUnionTypeNode(node)) { - return { k: "union", parts: node.types.map((t) => typeShape(file, t, bindings)) }; + return { k: "union", parts: node.types.nodes.map((t) => typeShape(file, t, bindings)) }; } if (ts.isTypeLiteralNode(node)) { let bad: string | null = null; const fields = shapeOfMembers( file, - node.members, + node.members.nodes, (text) => { bad = text; }, @@ -292,16 +292,16 @@ function typeShape( const name = node.typeName.text; const global = CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings); - if (global && name === "Uint8Array" && (node.typeArguments?.length ?? 0) === 0) + if (global && name === "Uint8Array" && (node.typeArguments?.nodes.length ?? 0) === 0) return { k: "bytes" }; if ( global && (name === "Array" || name === "ReadonlyArray") && - node.typeArguments?.length === 1 + node.typeArguments?.nodes.length === 1 ) { - return { k: "array", elem: typeShape(file, node.typeArguments[0]!, bindings) }; + return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) }; } - if ((node.typeArguments?.length ?? 0) > 0) + if ((node.typeArguments?.nodes.length ?? 0) > 0) return { k: "unsupported", text: node.getText(file) }; return { k: "ref", name }; } @@ -328,7 +328,7 @@ function stringArray(expr: ts.Expression): string[] | null { const e = unwrapConst(expr); if (!ts.isArrayLiteralExpression(e)) return null; const out: string[] = []; - for (const el of e.elements) { + for (const el of e.elements.nodes) { const v = unwrapConst(el as ts.Expression); if (!ts.isStringLiteral(v)) return null; out.push(v.text); @@ -340,12 +340,12 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null const e = unwrapConst(expr); if (!ts.isArrayLiteralExpression(e)) return null; const out: { env: string; msg: string }[] = []; - for (const el of e.elements) { + for (const el of e.elements.nodes) { const v = unwrapConst(el as ts.Expression); if (!ts.isObjectLiteralExpression(v)) return null; let env: string | null = null; let msg: string | null = null; - for (const prop of v.properties) { + for (const prop of v.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = propName(prop.name); const val = unwrapConst(prop.initializer); @@ -410,7 +410,7 @@ export function entryContractFacts( * and the refusal names every site. */ const collectTypes = (file: ts.SourceFile): void => { const exportedHere = new Set(); - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if ( (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) && isExported(stmt) @@ -418,7 +418,7 @@ export function entryContractFacts( exportedHere.add(stmt.name.text); } } - for (const stmt of file.statements) { + for (const stmt of file.statements.nodes) { if (ts.isInterfaceDeclaration(stmt) || ts.isTypeAliasDeclaration(stmt)) { const name = stmt.name.text; const exported = isExported(stmt); @@ -433,14 +433,14 @@ export function entryContractFacts( let bad: string | null = null; const fields = shapeOfMembers( file, - stmt.members, + stmt.members.nodes, (text) => { bad = text; }, bindings, ); - const generic = (stmt.typeParameters?.length ?? 0) > 0; - const heritage = (stmt.heritageClauses?.length ?? 0) > 0; + const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0; + const heritage = (stmt.heritageClauses?.nodes.length ?? 0) > 0; facts.types.push({ name, form: "interface", @@ -455,7 +455,7 @@ export function entryContractFacts( loc: locOf(file, stmt), }); } else { - const generic = (stmt.typeParameters?.length ?? 0) > 0; + const generic = (stmt.typeParameters?.nodes.length ?? 0) > 0; facts.types.push({ name, form: "alias", @@ -473,7 +473,7 @@ export function entryContractFacts( stmt.body !== undefined && ts.isModuleBlock(stmt.body) ) { - for (const inner of stmt.body.statements) { + for (const inner of stmt.body.statements.nodes) { if (ts.isInterfaceDeclaration(inner) || ts.isTypeAliasDeclaration(inner)) { addSite(inner.name.text, siteOf(file, inner)); } @@ -494,25 +494,25 @@ export function entryContractFacts( if (list.length > 1) facts.multiSiteTypes.push({ name, sites: list }); } - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) { if (!isExported(stmt)) continue; if (stmt.body === undefined && facts.functions.some((f) => f.name === stmt.name!.text)) continue; // overload signature facts.functions.push({ name: stmt.name.text, - params: stmt.parameters.map((p) => ({ + params: stmt.parameters.nodes.map((p) => ({ name: ts.isIdentifier(p.name) ? p.name.text : "", shape: p.type !== undefined ? typeShape(entry, p.type, bindings) : null, })), returns: stmt.type !== undefined ? typeShape(entry, stmt.type, bindings) : null, - generic: (stmt.typeParameters?.length ?? 0) > 0, + generic: (stmt.typeParameters?.nodes.length ?? 0) > 0, loc: locOf(entry, stmt), }); continue; } if (ts.isVariableStatement(stmt) && isExported(stmt)) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name)) continue; const name = decl.name.text; if (!CONVENTION_CONSTS.has(name)) continue; diff --git a/packages/compiler/src/frontend/lib-exports.ts b/packages/compiler/src/frontend/lib-exports.ts index 4839ff97f..738dd2f19 100644 --- a/packages/compiler/src/frontend/lib-exports.ts +++ b/packages/compiler/src/frontend/lib-exports.ts @@ -26,7 +26,7 @@ export interface EntryExportInfo { * Overload signatures collapse onto one entry (same name, one body). */ export function entryFunctionExports(entry: ts.SourceFile): Map { const out = new Map(); - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (!ts.isFunctionDeclaration(stmt) || stmt.name === undefined) continue; const mods = ts.canHaveModifiers(stmt) ? ts.getModifiers(stmt) : undefined; const exported = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true; @@ -36,7 +36,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map 0, + generic: (stmt.typeParameters?.nodes.length ?? 0) > 0, async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true, generator: stmt.asteriskToken !== undefined, }); diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index 3c7f96baa..feeb23019 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -110,14 +110,14 @@ export function isAssertClassNew(lowerer: Lowerer, expr: ts.Expression): boolean /** The options of `new Assert()`, or null when an option is * not a literal or the argument list has another shape. */ export function literalAssertOptions(expr: ts.NewExpression): AssertInstanceOptions | null { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const options: AssertInstanceOptions = { strict: true, diff: "simple", skipPrototype: false }; if (args.length === 0) return options; if (args.length > 1) return null; const arg = stripTypeCasts(args[0]!); if (ts.isIdentifier(arg) && arg.text === "undefined") return options; if (!ts.isObjectLiteralExpression(arg)) return null; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null; diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts index 7bb5f3e62..7e1852600 100644 --- a/packages/compiler/src/frontend/lowering/builtin-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts @@ -24,7 +24,7 @@ export function lowerAddAbortListener( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`addAbortListener with this argument list`, expr); } diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 00aac2794..5e21f6cb3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -192,7 +192,7 @@ export function literalBoolOptions( if (!ts.isObjectLiteralExpression(node)) return null; const bools: Record = {}; const exprs: Record = {}; - for (const p of node.properties) { + for (const p of node.properties.nodes) { if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name)) return null; if (exprKeys.includes(p.name.text)) { exprs[p.name.text] = p.initializer; diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index 27eb3cb87..708284be1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -106,14 +106,14 @@ export function lowerDiagnosticsChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "publish" && call.arguments.length === 1) { + if (name === "publish" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const msg = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const msg = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.publish", args: [receiver, msg], type: VOID, loc }; } - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.chanSubscribe", args: [receiver, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.chanUnsubscribe", args: [receiver, cb], type: BOOL, loc }; @@ -126,18 +126,18 @@ export function lowerDiagnosticsChannelMethodCall( // this/arguments to fn exactly like the trace calls. if ( (name === "bindStore" || name === "unbindStore") && - call.arguments.length >= 1 && - call.arguments.length <= (name === "bindStore" ? 2 : 1) + call.arguments.nodes.length >= 1 && + call.arguments.nodes.length <= (name === "bindStore" ? 2 : 1) ) { - if (!isBuiltinClassInstance(lowerer, call.arguments[0]!, "AsyncLocalStorage")) { + if (!isBuiltinClassInstance(lowerer, call.arguments.nodes[0]!, "AsyncLocalStorage")) { lowerer.noLowering( `Channel.${name} with this store argument`, - call.arguments[0]!, + call.arguments.nodes[0]!, "an AsyncLocalStorage instance (node:async_hooks) is the supported store", ); } const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const store = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const store = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); if (name === "unbindStore") { return { kind: "libCall", @@ -148,8 +148,8 @@ export function lowerDiagnosticsChannelMethodCall( }; } const transform: IrExpr = - call.arguments[1] !== undefined - ? lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!) + call.arguments.nodes[1] !== undefined + ? lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!) : dynUndefinedExpr(loc); return { kind: "libCall", @@ -159,15 +159,15 @@ export function lowerDiagnosticsChannelMethodCall( loc, }; } - if (name === "runStores" && call.arguments.length >= 2) { + if (name === "runStores" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const data = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const data = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const thisArg: IrExpr = - call.arguments[2] !== undefined - ? lowerDiagnosticsMessage(lowerer, call.arguments[2]!) + call.arguments.nodes[2] !== undefined + ? lowerDiagnosticsMessage(lowerer, call.arguments.nodes[2]!) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3), loc); return { kind: "libCall", fn: "dc.chanRunStores", @@ -198,7 +198,7 @@ export function lowerAsyncLocalStorageMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "getStore" && call.arguments.length === 0) { + if (name === "getStore" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", @@ -208,11 +208,11 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "run" && call.arguments.length >= 2) { + if (name === "run" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(2), loc); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(2), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.run", @@ -221,10 +221,10 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "exit" && call.arguments.length >= 1) { + if (name === "exit" && call.arguments.nodes.length >= 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(1), loc); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(1), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.exitRun", @@ -233,12 +233,12 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "enterWith" && call.arguments.length === 1) { + if (name === "enterWith" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "als.enterWith", args: [receiver, value], type: VOID, loc }; } - if (name === "disable" && call.arguments.length === 0) { + if (name === "disable" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return { kind: "libCall", fn: "als.disable", args: [receiver], type: VOID, loc }; } @@ -301,13 +301,13 @@ function lowerTracingHandlers(lowerer: Lowerer, node: ts.Expression): IrExpr { if ( ts.isObjectLiteralExpression(e) && lowerer.mapTypeOf(lowerer.typeOf(e))?.kind === "record" && - e.properties.length > 0 && - e.properties.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) + e.properties.nodes.length > 0 && + e.properties.nodes.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) ) { const loc = locOf(e); return { kind: "dynObjLit", - fields: e.properties.map((p) => { + fields: e.properties.nodes.map((p) => { const pa = p as ts.PropertyAssignment; return { key: { @@ -366,32 +366,32 @@ export function lowerTracingChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const handlers = lowerTracingHandlers(lowerer, call.arguments[0]!); + const handlers = lowerTracingHandlers(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.tcSubscribe", args: [receiver, handlers], type: VOID, loc } : { kind: "libCall", fn: "dc.tcUnsubscribe", args: [receiver, handlers], type: BOOL, loc }; } if ( (name === "traceSync" || name === "traceCallback" || name === "tracePromise") && - call.arguments.length >= 1 + call.arguments.nodes.length >= 1 ) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); // traceSync(fn, context?, thisArg?, ...args) / // tracePromise(fn, context?, thisArg?, ...args) / // traceCallback(fn, position?, context?, thisArg?, ...args) const shift = name === "traceCallback" ? 1 : 0; - const ctxNode = call.arguments[1 + shift]; - const thisNode = call.arguments[2 + shift]; + const ctxNode = call.arguments.nodes[1 + shift]; + const thisNode = call.arguments.nodes[2 + shift]; const ctx: IrExpr = ctxNode !== undefined ? lowerDiagnosticsMessage(lowerer, ctxNode) : { kind: "dynObjLit", fields: [], type: DYN, loc }; const thisArg: IrExpr = thisNode !== undefined ? lowerDiagnosticsMessage(lowerer, thisNode) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3 + shift), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3 + shift), loc); if (name === "traceSync") { return { kind: "libCall", @@ -416,8 +416,8 @@ export function lowerTracingChannelMethodCall( }; } const pos: IrExpr = - call.arguments[1] !== undefined - ? lowerer.lowerExprExpecting(call.arguments[1]!, F64) + call.arguments.nodes[1] !== undefined + ? lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64) : { kind: "numLit", value: -1, type: F64, loc }; return { kind: "libCall", @@ -473,29 +473,32 @@ export function lowerDiagnosticsChannelCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - if (bi.member === "channel" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "channel" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.channel", args: [name], type: F64, loc }; } - if ((bi.member === "subscribe" || bi.member === "unsubscribe") && expr.arguments.length === 2) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); - const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments[1]!); + if ( + (bi.member === "subscribe" || bi.member === "unsubscribe") && + expr.arguments.nodes.length === 2 + ) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments.nodes[1]!); return bi.member === "subscribe" ? { kind: "libCall", fn: "dc.subscribe", args: [name, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.unsubscribe", args: [name, cb], type: BOOL, loc }; } - if (bi.member === "hasSubscribers" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "hasSubscribers" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.hasSubscribers", args: [name], type: BOOL, loc }; } // tracingChannel: the string form interns the five tracing::* // channels; the collection form takes an object literal whose five // event members are Channel-typed values (Node's TracingChannelCollection). - if (bi.member === "tracingChannel" && expr.arguments.length === 1) { - const a = expr.arguments[0]!; + if (bi.member === "tracingChannel" && expr.arguments.nodes.length === 1) { + const a = expr.arguments.nodes[0]!; if (ts.isObjectLiteralExpression(a)) { const byEvent = new Map(); - for (const p of a.properties) { + for (const p of a.properties.nodes) { if ( !ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name) || diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index b4a394dce..41e35a511 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -45,9 +45,9 @@ export function lowerAtomicsCall( : 3; const maximum = member === "wait" ? 4 : member === "notify" ? 3 : minimum; if ( - call.arguments.length < minimum || - call.arguments.length > maximum || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < minimum || + call.arguments.nodes.length > maximum || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Atomics.${member} arguments`, @@ -55,7 +55,7 @@ export function lowerAtomicsCall( `use ${minimum}${maximum > minimum ? `–${maximum}` : ""} arguments`, ); } - const source = call.arguments[0]!; + const source = call.arguments.nodes[0]!; // Generic constraints can remain unions in the checker while this body // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); @@ -75,8 +75,8 @@ export function lowerAtomicsCall( } const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); const arg = (index: number, fallback: number): IrExpr => - call.arguments[index] - ? lowerOptionalArgument(lowerer, call.arguments[index]!, F64, number(fallback)) + call.arguments.nodes[index] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[index]!, F64, number(fallback)) : number(fallback); const index = arg(1, 0); if (member === "wait") diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts index 14d3c6cce..3fc4ec173 100644 --- a/packages/compiler/src/frontend/lowering/builtins/blob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts @@ -70,7 +70,7 @@ function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpr export function lowerBlobNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const cls = blobClassOf(lowerer, expr.expression); if (cls === null) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); const values = args.map((arg) => @@ -94,10 +94,10 @@ export function lowerStaticBlobCall(lowerer: Lowerer, call: ts.CallExpression): const member = access.name.text; if (access.questionDotToken || !BLOB_CALLS.has(member)) return null; if (blobReceiverClass(lowerer, access.expression) === null) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; const loc = locOf(call); const receiver = lowerer.lowerExprExpecting(access.expression, DYN); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); if (member === "slice") { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index cf19b4003..1669978af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -126,7 +126,7 @@ export function registerPromisifiedBuiltinDecl( if (registerHttpClientFnBinding(lowerer, nameNode, init)) return true; const e = isPromisifyCall(lowerer, init); if (!e) return false; - const argNode = e.arguments.length === 1 ? e.arguments[0]! : null; + const argNode = e.arguments.nodes.length === 1 ? e.arguments.nodes[0]! : null; // `promisify(exec)` over a named import, or `promisify(cp.exec)` over a // namespace import. const target = !argNode diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 04b3caba8..5590fbe9f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -42,9 +42,9 @@ export function lowerExecFileAsyncCall( ): IrExpr { const what = shell ? "the promisified exec" : "the promisified execFile"; if ( - expr.arguments.length < 1 || - expr.arguments.length > (shell ? 2 : 3) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > (shell ? 2 : 3) || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `${what} with this argument shape`, @@ -54,7 +54,7 @@ export function lowerExecFileAsyncCall( : "the supported form is execFileAsync(file, args?, options?)", ); } - const command = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const command = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // exec: /bin/sh -c , execSync's lowering (the command string // rides as the argv[1] the failure display reads); execFile: file + its // args list. @@ -66,8 +66,8 @@ export function lowerExecFileAsyncCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[shell ? 1 : 2]; + : lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[shell ? 1 : 2]; const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; const helper = execFileAsyncHelper(lowerer, loc, shell); @@ -93,7 +93,7 @@ export function lowerExecFileAsyncCall( "pass the options inline so each member can be checked", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the portless // opensslAsync shape: `...(c ? { env: { ...process.env, ...extra // } } : {})` (either orientation). The condition evaluates ONCE diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index 1fc015892..a435bf740 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -35,22 +35,22 @@ export function lowerChildMethodCall( const loc = locOf(call); if (name === "send") { if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `child.send with ${call.arguments.length} arguments`, + `child.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } const receiver = lowerer.lowerExpr(access.expression); - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "child.send", args: [receiver, json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "child.sendCb", @@ -59,7 +59,7 @@ export function lowerChildMethodCall( loc, }; } - if (name === "disconnect" && call.arguments.length === 0) { + if (name === "disconnect" && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -75,8 +75,8 @@ export function lowerChildMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if ( event !== "spawn" && @@ -88,7 +88,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( `child.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)', ); } @@ -105,7 +105,7 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onMessage", - args: [receiver, ipcMessageListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; @@ -114,24 +114,24 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onDisconnect", - args: [receiver, ipcDisconnectListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; } - const listenerNode = stripTypeCasts(call.arguments[1]!); + const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( isJsSourceFile(listenerNode.getSourceFile()) && (ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) && - listenerNode.parameters.some((param) => param.dotDotDotToken) && - listenerNode.parameters.every((param) => !param.type) && + listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) && + listenerNode.parameters.nodes.every((param) => !param.type) && !/@(?:param|type)\b/.test( listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()), ) ) { - for (const param of listenerNode.parameters) lowerer.checkedCallbackParams.add(param); + for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param); } - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); if ( (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") || (cb.type.kind === "func" && @@ -149,7 +149,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( "child event listeners with non-representable parameters", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { @@ -168,7 +168,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { lowerer.noLowering( "spawn event listeners with parameters or a returned value", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; @@ -176,7 +176,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event !== "error" ? `${event} listeners with more than two parameters (use (code, signal), (code), or ())` : "error listeners with more than one parameter (use (err) or ())", @@ -188,7 +188,7 @@ export function lowerChildMethodCall( // fenced instead of being called through the wrong ABI. lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "listeners returning a value (make the callback body a block, or return nothing)", ); } @@ -206,7 +206,7 @@ export function lowerChildMethodCall( if (!armsOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose parameter is not 'number | null' (got '${lowerer.fmt(param!)}')`, ); } @@ -228,7 +228,7 @@ export function lowerChildMethodCall( if (!sigOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`, ); } @@ -243,7 +243,7 @@ export function lowerChildMethodCall( if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } @@ -256,11 +256,11 @@ export function lowerChildMethodCall( // reaped or never spawned (Node's null-handle answer), and a successful // send sets `killed`. if (name === "kill") { - if (call.arguments.length > 1) { - lowerer.noLowering(`child.kill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`child.kill with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const sigNode = call.arguments[0]; + const sigNode = call.arguments.nodes[0]; if (!sigNode) { const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc }; return { kind: "libCall", fn: "child.kill", args: [receiver, dflt], type: BOOL, loc }; @@ -281,7 +281,7 @@ export function lowerChildMethodCall( // child.unref(): drops the child from the event loop's keep-alive set // (the process may exit while the child runs — Node's semantics; the // child is still reaped while the loop runs for other reasons). - if ((name === "unref" || name === "ref") && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", @@ -320,9 +320,9 @@ export function lowerChildStreamMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "setEncoding" && call.arguments.length === 1) { + if (name === "setEncoding" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, CHILDSTREAM_T); - const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments[0]!); + const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments.nodes[0]!); return { kind: "libCall", fn: "stream.childSetEncoding", @@ -331,13 +331,13 @@ export function lowerChildStreamMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end") { lowerer.noLowering( `stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"data" and "end" are the supported child-stream events (as literals)', ); } @@ -349,7 +349,7 @@ export function lowerChildStreamMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc }; if ( cb.type.kind !== "func" || @@ -358,7 +358,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "data" ? "data listeners with more than one parameter or a return value (use (chunk) or ())" : "end listeners with parameters or a return value (use ())", @@ -381,7 +381,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`, ); } @@ -417,15 +417,15 @@ export function lowerChildWriterMethodCall( const loc = locOf(call); if (name === "write") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `child stdin write with ${call.arguments.length} arguments`, + `child stdin write with ${call.arguments.nodes.length} arguments`, call, "write(string | Uint8Array) with no encoding or callback is supported", ); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const data = lowerer.lowerExpr(call.arguments[0]!); + const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind === "string") { return { kind: "libCall", fn: "writer.writeString", args: [receiver, data], type: BOOL, loc }; } @@ -434,12 +434,12 @@ export function lowerChildWriterMethodCall( } lowerer.noLowering( `child stdin write of '${lowerer.fmt(data.type)}'`, - call.arguments[0]!, + call.arguments.nodes[0]!, "write one string, Buffer, or Uint8Array value (narrow unions first)", ); } - if ((name === "end" || name === "destroy") && call.arguments.length === 0) { + if ((name === "end" || name === "destroy") && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -457,13 +457,13 @@ export function lowerChildWriterMethodCall( }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const eventType = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const eventType = lowerer.typeOf(call.arguments.nodes[0]!); const event = eventType.isStringLiteralType() ? eventType.value : null; if (event !== "drain" && event !== "finish" && event !== "error") { lowerer.noLowering( `child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"drain", "finish", and "error" are the supported child stdin events', ); } @@ -471,7 +471,7 @@ export function lowerChildWriterMethodCall( lowerer.unsupported("SC1090", call, "chaining child stdin listener registration"); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const maxParams = event === "error" ? 1 : 0; if ( cb.type.kind !== "func" || @@ -480,7 +480,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "error" ? "error listeners with at most one Error parameter and no return value" : `${event} listeners with no parameters or return value`, @@ -493,7 +493,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "child stdin error listeners whose parameter is Error", ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index bfa2bc99a..24adcc1ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -48,15 +48,15 @@ export function lowerExecSyncCall( shell: boolean, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${shell ? "execSync" : "execFileSync"} with a spread call`, expr); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // execSync: /bin/sh -c ; execFileSync: file + its args list. let argvExpr: IrExpr; let optsNode: ts.Expression | undefined; if (shell) { - if (expr.arguments.length > 2) { + if (expr.arguments.nodes.length > 2) { lowerer.noLowering( "execSync with this argument shape", expr, @@ -69,17 +69,17 @@ export function lowerExecSyncCall( type: arrayOf(STRING), loc, }; - optsNode = expr.arguments[1]; + optsNode = expr.arguments.nodes[1]; } else { - if (expr.arguments.length > 3) { + if (expr.arguments.nodes.length > 3) { lowerer.noLowering( "execFileSync with this argument shape", expr, "the supported form is execFileSync(file, args?, options?)", ); } - argvExpr = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - optsNode = expr.arguments[2]; + argvExpr = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + optsNode = expr.arguments.nodes[2]; } // The shell command itself is /bin/sh; the "command" string rides as // the argv[1] the display formatter reads. @@ -186,8 +186,8 @@ export function lowerExecSyncCall( modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering( @@ -206,7 +206,7 @@ export function lowerExecSyncCall( ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the openssl-runner // shape: `...(c ? { env: { ...process.env, ...extra } } : {})` // (either orientation). The condition evaluates ONCE and picks diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ecec14fc5..ea2dd379d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -31,8 +31,8 @@ export function lowerChildArgsArg( loc: SrcLoc, ): IrExpr { if (!node) return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }; - if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) { - const elems = node.elements.map((el) => lowerer.lowerExprExpecting(el, STRING)); + if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) { + const elems = node.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, STRING)); return { kind: "arrayLit", elems, type: arrayOf(STRING), loc: locOf(node) }; } return lowerer.lowerExprExpecting(node, arrayOf(STRING)); @@ -92,16 +92,16 @@ const NODE_SIGNAL_NAMES = new Set([ * fences by name. The bare `{ encoding: "utf8" }` shape keeps its * historical cp.spawnSync lowering. */ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawnSync with this argument shape", expr, "the supported form is spawnSync(command, args?, options?)", ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const argv = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[2]; + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const argv = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[2]; let timeout: IrExpr = numLit(0, loc); let killSignal: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; @@ -152,8 +152,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering("spawnSync stdio tuple entries beyond string literals", el); @@ -182,7 +182,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo 'stdio takes a "pipe"/"ignore"/"inherit" literal (or a value typed as a union of those), or a 3-tuple of literals', ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -280,21 +280,21 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo * `windowsHide` (a POSIX no-op). The bare `{ stdio: "ignore" }` shape * keeps its historical cp.spawn lowering. */ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawn with this argument shape", expr, 'the supported form is spawn(command, args?, { stdio: "pipe" | "ignore" | "inherit", detached?, env?, cwd? })', ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -338,7 +338,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S let plain = true; // exactly { stdio: "ignore" }: the historical libCall if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom `...(isWindows ? {} : { detached: // true })` (either orientation): the one carried member supported // is `detached` with a boolean literal — the platform-conditional @@ -393,7 +393,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S // The 3-tuple form: every slot accepts a literal, including // "pipe" (child.stdin/stdout/stderr); stdout/stderr also accept // a number-typed fd (an openSync result — dup2'd into the child). - if (ts.isArrayLiteralExpression(m.value) && m.value.elements.length === 3) { + if (ts.isArrayLiteralExpression(m.value) && m.value.elements.nodes.length === 3) { const slot = (el: ts.Expression, which: 0 | 1 | 2): void => { const t = lowerer.typeOf(el); if (t.isStringLiteralType()) { @@ -434,7 +434,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S errFd = fd; } }; - m.value.elements.forEach((el, i) => slot(el, i as 0 | 1 | 2)); + m.value.elements.nodes.forEach((el, i) => slot(el, i as 0 | 1 | 2)); sawStdio = true; plain = false; break; @@ -512,7 +512,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S } if (shellEnabled && argsNode !== undefined) { const shellArgs = stripTypeCasts(argsNode); - if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.length !== 0) { + if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.nodes.length !== 0) { lowerer.noLowering( "spawn with shell enabled and separate arguments", argsNode, @@ -551,9 +551,9 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S * Node's default JSON IPC channel is always present. */ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fork with this argument shape", @@ -561,24 +561,24 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr "use fork(staticModulePath[, stringArgs][, inlineOptions])", ); } - const targetPath = staticForkModulePath(lowerer.program, expr.arguments[0]!); + const targetPath = staticForkModulePath(lowerer.program, expr.arguments.nodes[0]!); const targetId = targetPath === null ? undefined : lowerer.forkTargetIdByPath.get(tsgoPath(resolve(targetPath))); if (targetId === undefined) { lowerer.noLowering( "fork with a runtime-valued module path", - expr.arguments[0]!, + expr.arguments.nodes[0]!, 'resolve a relative worker with new URL("./worker.ts", import.meta.url) or fileURLToPath(...) and bind it to a const if needed', ); } let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -622,7 +622,7 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr }; if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const property of optsNode.properties) { + for (const property of optsNode.properties.nodes) { const member = optionMember(property); if (!member) { lowerer.noLowering( @@ -651,14 +651,14 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr break; case "stdio": { if (ts.isArrayLiteralExpression(member.value)) { - if (member.value.elements.length !== 4) { + if (member.value.elements.nodes.length !== 4) { lowerer.noLowering( "fork with this stdio tuple", member.value, 'use [stdin, stdout, stderr, "ipc"] with exactly four literal entries', ); } - const ipc = member.value.elements[3]!; + const ipc = member.value.elements.nodes[3]!; const ipcType = lowerer.typeOf(ipc); if (!ipcType.isStringLiteralType() || ipcType.value !== "ipc") { lowerer.noLowering( @@ -667,9 +667,9 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr 'the fourth stdio entry must be "ipc"', ); } - inMode = stdioMode(member.value.elements[0]!, "stdin"); - outMode = stdioMode(member.value.elements[1]!, "stdout"); - errMode = stdioMode(member.value.elements[2]!, "stderr"); + inMode = stdioMode(member.value.elements.nodes[0]!, "stdin"); + outMode = stdioMode(member.value.elements.nodes[1]!, "stdout"); + errMode = stdioMode(member.value.elements.nodes[2]!, "stderr"); } else { const mode = stdioMode(member.value, "scalar"); inMode = outMode = errMode = mode; @@ -743,20 +743,20 @@ export function lowerExecFileCall( ): IrExpr { const what = shell ? "exec" : "execFile"; if ( - expr.arguments.some(ts.isSpreadElement) || - expr.arguments.length < 2 || - expr.arguments.length > (shell ? 3 : 4) + expr.arguments.nodes.some(ts.isSpreadElement) || + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > (shell ? 3 : 4) ) { lowerer.noLowering( - `${what} with ${expr.arguments.length} arguments`, + `${what} with ${expr.arguments.nodes.length} arguments`, expr, shell ? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)" : "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)", ); } - const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const callbackNode = expr.arguments[expr.arguments.length - 1]!; + const first = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const callbackNode = expr.arguments.nodes[expr.arguments.nodes.length - 1]!; // exec: /bin/sh -c , execSync's lowering (the runtime reads the // command line back from argv[1] for failure messages); execFile: the // file and its args list. @@ -768,9 +768,15 @@ export function lowerExecFileCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc); + : lowerChildArgsArg( + lowerer, + expr.arguments.nodes.length >= 3 ? expr.arguments.nodes[1] : undefined, + loc, + ); const optionsNode = - expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined; + expr.arguments.nodes.length === (shell ? 3 : 4) + ? expr.arguments.nodes[shell ? 1 : 2] + : undefined; if (optionsNode) { const options = optionsNode; if (!ts.isObjectLiteralExpression(options)) { @@ -792,7 +798,7 @@ export function lowerExecFileCall( ].includes(node.operatorToken.kind) && pureNumber(node.left) && pureNumber(node.right)); - for (const property of options.properties) { + for (const property of options.properties.nodes) { const member = optionMember(property); if (!member) lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 6de345dfe..0cd25cde6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -197,7 +197,7 @@ export function lowerZlibStreamMethodCall( ): IrExpr { const loc = locOf(call); const member = access.name.text; - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) lowerer.noLowering(`${member} with spread arguments`, call); const isFunc = (node: ts.Expression): boolean => lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "func"; @@ -304,25 +304,25 @@ export function lowerPromisifiedZlibCall( member: string, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified zlib.${member} with spread arguments`, expr); } - if (expr.arguments.length !== 1 && expr.arguments.length !== 2) { + if (expr.arguments.nodes.length !== 1 && expr.arguments.nodes.length !== 2) { lowerer.noLowering( - `the promisified zlib.${member} with ${expr.arguments.length} arguments`, + `the promisified zlib.${member} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${member}Async(data) and ${member}Async(data, options)`, ); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); return { kind: "libCall", fn: input.dyn ? "zlib.promiseDyn" : "zlib.promise", args: [ numLit(ZLIB_ONE_SHOT_MODES[member]!, loc), input.value, - expr.arguments[1] !== undefined - ? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${member}`, loc) + expr.arguments.nodes[1] !== undefined + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${member}`, loc) : dynUndefinedExpr(loc), ], type: { kind: "promise", inner: BYTES_U8 }, @@ -346,19 +346,26 @@ export function lowerZlibModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`zlib.${bi.member} with spread arguments`, expr); } const streamMode = ZLIB_STREAM_FACTORIES[bi.member]; if (streamMode !== undefined) { - return lowerZlibStreamNew(lowerer, expr.arguments, streamMode, `zlib.${bi.member}`, expr, loc); + return lowerZlibStreamNew( + lowerer, + expr.arguments.nodes, + streamMode, + `zlib.${bi.member}`, + expr, + loc, + ); } const syncFn = ZLIB_SYNC_FNS[bi.member]; if (syncFn !== undefined) { - if (expr.arguments.length === 0 || expr.arguments.length > 2) { - lowerer.noLowering(`${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length === 0 || expr.arguments.nodes.length > 2) { + lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); if (input.dyn) { return { kind: "libCall", @@ -366,8 +373,8 @@ export function lowerZlibModuleCall( args: [ numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, - expr.arguments[1] !== undefined - ? zlibOptionsValue(lowerer, expr.arguments[1], `zlib.${bi.member}`, loc) + expr.arguments.nodes[1] !== undefined + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1], `zlib.${bi.member}`, loc) : dynUndefinedExpr(loc), ], type: BYTES_U8, @@ -375,15 +382,15 @@ export function lowerZlibModuleCall( }; } if ( - expr.arguments.length === 2 && + expr.arguments.nodes.length === 2 && (bi.member === "deflateSync" || bi.member === "deflateRawSync" || bi.member === "gzipSync") ) { - const options = expr.arguments[1]!; + const options = expr.arguments.nodes[1]!; // A lone literal level keeps the direct compression entry (build-time // compression in the native emitters uses it); other options take the // runtime option ladder below. - if (ts.isObjectLiteralExpression(options) && options.properties.length === 1) { - const prop = options.properties[0]!; + if (ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) { + const prop = options.properties.nodes[0]!; if ( ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && @@ -416,14 +423,14 @@ export function lowerZlibModuleCall( } } } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return { kind: "libCall", fn: "zlib.syncOpts", args: [ numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, - zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc), + zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc), ], type: BYTES_U8, loc, @@ -439,19 +446,19 @@ export function lowerZlibModuleCall( } const callbackFn = ZLIB_CALLBACK_FNS[bi.member]; if (callbackFn !== undefined) { - if (expr.arguments.length !== 2 && expr.arguments.length !== 3) { + if (expr.arguments.nodes.length !== 2 && expr.arguments.nodes.length !== 3) { lowerer.noLowering( - `${bi.member} with ${expr.arguments.length} arguments`, + `${bi.member} with ${expr.arguments.nodes.length} arguments`, expr, `${bi.member}(data, callback) and ${bi.member}(data, options, callback) are supported`, ); } - const input = zlibInput(lowerer, expr.arguments[0]!, loc); + const input = zlibInput(lowerer, expr.arguments.nodes[0]!, loc); const dynData = input.dyn; - if (expr.arguments.length === 3 || dynData) { + if (expr.arguments.nodes.length === 3 || dynData) { // (data, options, callback): Node validates the options before the // callback and the input, so the runtime ladder runs first. - const withOptions = expr.arguments.length === 3; + const withOptions = expr.arguments.nodes.length === 3; return { kind: "libCall", fn: dynData ? "zlib.cbOptsDyn" : "zlib.cbOpts", @@ -459,9 +466,9 @@ export function lowerZlibModuleCall( numLit(ZLIB_ONE_SHOT_MODES[bi.member]!, loc), input.value, withOptions - ? zlibOptionsValue(lowerer, expr.arguments[1]!, `zlib.${bi.member}`, loc) + ? zlibOptionsValue(lowerer, expr.arguments.nodes[1]!, `zlib.${bi.member}`, loc) : dynUndefinedExpr(loc), - errorFirstBytesCallback(lowerer, expr.arguments[withOptions ? 2 : 1]!, "zlib"), + errorFirstBytesCallback(lowerer, expr.arguments.nodes[withOptions ? 2 : 1]!, "zlib"), ], type: VOID, loc, @@ -470,15 +477,15 @@ export function lowerZlibModuleCall( return { kind: "libCall", fn: callbackFn, - args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments[1]!, "zlib")], + args: [input.value, errorFirstBytesCallback(lowerer, expr.arguments.nodes[1]!, "zlib")], type: VOID, loc, }; } if (bi.member === "crc32") { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `zlib.crc32 with ${expr.arguments.length} arguments`, + `zlib.crc32 with ${expr.arguments.nodes.length} arguments`, expr, "crc32(data[, initialValue]) is supported", ); @@ -487,9 +494,9 @@ export function lowerZlibModuleCall( kind: "libCall", fn: "zlib.crc32", args: [ - lowerBuiltinByteInput(lowerer, expr.arguments[0]!, loc, "zlib"), - expr.arguments[1] - ? lowerer.lowerExprExpecting(expr.arguments[1]!, F64) + lowerBuiltinByteInput(lowerer, expr.arguments.nodes[0]!, loc, "zlib"), + expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, F64) : { kind: "numLit", value: 0, type: F64, loc }, ], type: F64, diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index 0325a0ef7..dd9fcf987 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -88,7 +88,7 @@ export function builtinConstantsDestructureDecl( const module = builtinConstantsModuleOf(lowerer, init); if (module === null) return false; const table = BUILTIN_CONSTANTS_TABLES[module]!.table; - return nameNode.elements.every( + return nameNode.elements.nodes.every( (el) => !el.dotDotDotToken && el.initializer === undefined && diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index bfac6128f..575748ef6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -47,17 +47,17 @@ export function lowerCryptoComposedCall( // "hex"/"base64" encoding. Anything else falls through (null): bare // randomBytes lowers to a real Buffer through the crypto table, and // the .toString rides the ordinary Buffer method lowering. - if (recv.arguments.length !== 1) return null; - const encNode = call.arguments[0]; + if (recv.arguments.nodes.length !== 1) return null; + const encNode = call.arguments.nodes[0]; const encT = encNode ? lowerer.typeOf(encNode) : undefined; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || !encT?.isStringLiteralType() || (encT.value !== "hex" && encT.value !== "base64") ) { return null; } - const size = lowerer.lowerExprExpecting(recv.arguments[0]!, F64); + const size = lowerer.lowerExprExpecting(recv.arguments.nodes[0]!, F64); const enc = lowerer.lowerExprExpecting(encNode!, STRING); return { kind: "libCall", @@ -96,28 +96,30 @@ function lowerHashDigestChain( : null; if (!bi || bi.module !== "crypto" || bi.member !== "createHash") return null; const loc = locOf(call); - const algT = chCall.arguments.length === 1 ? lowerer.typeOf(chCall.arguments[0]!) : undefined; + const algT = + chCall.arguments.nodes.length === 1 ? lowerer.typeOf(chCall.arguments.nodes[0]!) : undefined; if (!algT?.isStringLiteralType() || (algT.value !== "sha256" && algT.value !== "sha1")) return null; - if (updateCall.arguments.length !== 1) { + if (updateCall.arguments.nodes.length !== 1) { return null; } - const encT = call.arguments.length === 1 ? lowerer.typeOf(call.arguments[0]!) : undefined; + const encT = + call.arguments.nodes.length === 1 ? lowerer.typeOf(call.arguments.nodes[0]!) : undefined; if (!encT?.isStringLiteralType() || (encT.value !== "hex" && encT.value !== "base64")) { return null; } // alg and enc are proven literals (fenced above), so lowering them // out of source position observes nothing; the data lowers between // them in its own source order. - const alg = lowerer.lowerExprExpecting(chCall.arguments[0]!, STRING); + const alg = lowerer.lowerExprExpecting(chCall.arguments.nodes[0]!, STRING); // The data picks the runtime entry by its static type, the // fileURLToPath convention: strings hash their UTF-8 bytes (Node's // default input encoding), Buffers/typed arrays hash their bytes. - const dataNode = updateCall.arguments[0]!; + const dataNode = updateCall.arguments.nodes[0]!; const dataIr = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); if (dataIr?.kind === "bytes") { const data = cryptoByteView(lowerer, dataNode, lowerer.lowerExpr(dataNode)); - const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "crypto.hashDigestBytes", @@ -128,7 +130,7 @@ function lowerHashDigestChain( } if (dataIr?.kind === "string") { const data = lowerer.lowerExprExpecting(dataNode, STRING); - const enc = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const enc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "crypto.hashDigestStr", @@ -257,7 +259,7 @@ export function lowerCryptoModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "crypto") return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.${bi.member} with spread arguments`, expr); } @@ -465,8 +467,8 @@ export function lowerCryptoModuleCall( let options = args[2]; // An options literal holding only a literal string-digest // outputEncoding is that encoding; evaluating it observes nothing. - if (options && ts.isObjectLiteralExpression(options) && options.properties.length === 1) { - const property = options.properties[0]!; + if (options && ts.isObjectLiteralExpression(options) && options.properties.nodes.length === 1) { + const property = options.properties.nodes[0]!; if ( ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && @@ -820,8 +822,8 @@ export function lowerCryptoModuleCall( }; const list = own(LISTS, bi.member); if (bi.member !== "getFips" && list === undefined) return null; - if (expr.arguments.length !== 0) { - lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) { + lowerer.noLowering(`crypto.${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } if (bi.member === "getFips") { return { kind: "numLit", value: 0, type: F64, loc }; @@ -916,7 +918,7 @@ export function lowerCipherMethodCall( "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not", ); } - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`Cipheriv/Decipheriv.${name} with spread arguments`, call); const loc = locOf(call); return { @@ -924,7 +926,7 @@ export function lowerCipherMethodCall( recv: lowerer.lowerExprExpecting(access.expression, DYN), method: name, calleeName: v8CalleeText(access), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -946,9 +948,9 @@ export function lowerCryptoHashMethodCall( // Hmac has no copy(): Node reads the member as undefined and throws a // TypeError once the arguments are evaluated. if (receiverType.kind === "cryptoHmac" && name === "copy") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("Hmac.copy with spread arguments", call); - const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({ + const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc, @@ -965,19 +967,19 @@ export function lowerCryptoHashMethodCall( const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType); const hash = receiverType.kind === "cryptoHash"; const label = hash ? "Hash" : "Hmac"; - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${label}.${name} with spread arguments`, call); if (name === "update") { - if (call.arguments.length < 1 || call.arguments.length > 2) { + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { lowerer.noLowering( - `${label}.update with ${call.arguments.length} arguments`, + `${label}.update with ${call.arguments.nodes.length} arguments`, call, "update(data[, inputEncoding]) is supported", ); } const target = receiver(); - const dataNode = call.arguments[0]!; - const encodingNode = call.arguments[1]; + const dataNode = call.arguments.nodes[0]!; + const encodingNode = call.arguments.nodes[1]; const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); // Indexed reads can retain optional storage after a nullish fallback // or narrowing. Use the proven argument type through a checked coercion. @@ -1032,14 +1034,14 @@ export function lowerCryptoHashMethodCall( }; } if (name === "digest") { - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { lowerer.noLowering( - `${label}.digest with ${call.arguments.length} arguments`, + `${label}.digest with ${call.arguments.nodes.length} arguments`, call, "digest([outputEncoding]) is supported", ); } - const encodingNode = call.arguments[0]; + const encodingNode = call.arguments.nodes[0]; if (encodingNode === undefined) { return { kind: "libCall", @@ -1078,13 +1080,13 @@ export function lowerCryptoHashMethodCall( }; } if (name === "copy" && hash) { - if (call.arguments.length > 1) + if (call.arguments.nodes.length > 1) lowerer.noLowering( - `Hash.copy with ${call.arguments.length} arguments`, + `Hash.copy with ${call.arguments.nodes.length} arguments`, call, "copy([options]) is supported", ); - if (call.arguments.length === 0) + if (call.arguments.nodes.length === 0) return { kind: "libCall", fn: "crypto.hashCopy", @@ -1095,7 +1097,7 @@ export function lowerCryptoHashMethodCall( return { kind: "libCall", fn: "crypto.hashCopyOpts", - args: [receiver(), lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [receiver(), lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], type: CRYPTOHASH_T, loc, }; @@ -1138,7 +1140,7 @@ export function lowerPromisifiedCryptoCall( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified crypto.${member} with spread arguments`, expr); } diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 9f0ab7a29..32fe09ab3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -53,8 +53,8 @@ export function lowerDateCall( if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); if (lowerer.stdlibGlobalMember(access, "Date") === "now") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`Date.now with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call); } return { kind: "libCall", fn: "date.now", args: [], type: F64, loc }; } @@ -62,10 +62,10 @@ export function lowerDateCall( // new Date(dateString).getTime(); keep their NaN and TimeClip behavior // identical for the ISO timestamps used by CLI applications. if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`Date.parse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); } - const input = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const input = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "date.parse", args: [input], type: F64, loc }; } // Date.UTC(year[, month[, date[, hours[, minutes[, seconds[, ms]]]]]]): @@ -75,16 +75,16 @@ export function lowerDateCall( // argument to number, so the seven-f64 ABI is exact. if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") { if ( - call.arguments.length < 1 || - call.arguments.length > 7 || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 7 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - lowerer.noLowering(`Date.UTC with ${call.arguments.length} arguments`, call); + lowerer.noLowering(`Date.UTC with ${call.arguments.nodes.length} arguments`, call); } const defaults = [0, 0, 1, 0, 0, 0, 0]; // year is always present (arity ≥ 1) const args: IrExpr[] = []; for (let i = 0; i < 7; i++) { - const a = call.arguments[i]; + const a = call.arguments.nodes[i]; args.push( a !== undefined ? lowerer.lowerExprExpecting(a, F64) @@ -97,7 +97,7 @@ export function lowerDateCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; if (name === "toJSON") return lowerDateToJson(lowerer, call, access); - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); } const raw = lowerer.lowerExpr(access.expression); @@ -147,8 +147,8 @@ function lowerDateToJson( access: ts.PropertyAccessExpression, ): IrExpr { const loc = locOf(call); - if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + lowerer.noLowering(`Date.prototype.toJSON with ${call.arguments.nodes.length} arguments`, call); const raw = lowerer.lowerExpr(access.expression); const receiver: IrExpr = raw.type.kind === "dyn" @@ -158,7 +158,7 @@ function lowerDateToJson( lowerer.badType(access.expression, lowerer.typeOf(access.expression)); const date = lowerer.declareHiddenLocal("%date", receiver.type); const stmts: IrStmt[] = [{ kind: "varDecl", localId: date.id, init: receiver, loc }]; - const key = call.arguments[0]; + const key = call.arguments.nodes[0]; if (key) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(key), loc }); const value: IrExpr = { kind: "varRef", localId: date.id, type: receiver.type, loc }; const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [value], type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 469a66ace..fb1c92257 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -22,7 +22,7 @@ export function lowerDisposableStackNew(lowerer: Lowerer, expr: ts.NewExpression if (!ts.isIdentifier(expr.expression) || expr.expression.text !== "DisposableStack") return null; const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - if ((expr.arguments ?? []).length !== 0) return null; + if ((expr.arguments?.nodes ?? []).length !== 0) return null; return { kind: "libCall", fn: "disposable.stackNew", args: [], type: DYN, loc: locOf(expr) }; } @@ -49,8 +49,8 @@ export function lowerDisposableStackUseCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "use" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (access.name.text !== "use" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (ts.isSpreadElement(argNode)) return null; if (!isStdlibDisposableStack(lowerer, access.expression)) return null; const valueT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); diff --git a/packages/compiler/src/frontend/lowering/builtins/environment.ts b/packages/compiler/src/frontend/lowering/builtins/environment.ts index 26c589dad..7d3e7ff91 100644 --- a/packages/compiler/src/frontend/lowering/builtins/environment.ts +++ b/packages/compiler/src/frontend/lowering/builtins/environment.ts @@ -78,12 +78,12 @@ export function lowerProcessEnvGet( } export function lowerProcessLoadEnvFile(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("process.loadEnvFile with spread or extra arguments", call); } const loc = locOf(call); - const path: IrExpr = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + const path: IrExpr = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const load: IrExpr = { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 50a97cead..1572aa6ab 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -209,8 +209,8 @@ export function lowerErrorIsErrorCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Error") !== "isError") return null; - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const caughtLocal = lowerer.caughtLocalOf(argNode); if (caughtLocal) { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index d6864a2d9..05028e1df 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -38,12 +38,12 @@ export function lowerFsCallbackCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("filesystem callback call with spread arguments", expr); const timestamp = ["utimes", "futimes", "lutimes"].includes(bi.member); const args: IrExpr = { kind: "dynArrLit", - elems: expr.arguments.map((arg, index) => + elems: expr.arguments.nodes.map((arg, index) => timestamp && (index === 1 || index === 2) ? lowerFsTimestampValue(lowerer, arg, loc) : lowerer.lowerExprExpecting(arg, DYN), @@ -71,11 +71,11 @@ export function lowerFsLinkOrStatfsCall( const symbolic = bi.member === "symlinkSync" || bi.member === "symlink"; const reading = bi.member === "readlinkSync" || bi.member === "readlink"; const arity = symbolic ? 3 : 2; - if (expr.arguments.length > arity || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > arity || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = Array.from({ length: arity }, (_, index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); if (reading) { @@ -117,11 +117,11 @@ export function lowerFsCpCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = [0, 1, 2].map((index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; @@ -134,18 +134,18 @@ export function lowerFsTimestampCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); - const path = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const path = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); return { kind: "libCall", fn: fn.fn, args: [ path, - lowerFsTimestampValue(lowerer, expr.arguments[1], loc), - lowerFsTimestampValue(lowerer, expr.arguments[2], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[1], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[2], loc), ], type: fn.result, loc, @@ -163,14 +163,14 @@ export function lowerFsVectorOrTruncateCall( const vector = bi.member !== "ftruncateSync"; const required = vector ? 2 : 1; if ( - expr.arguments.length < required || - expr.arguments.length > required + 1 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < required || + expr.arguments.nodes.length > required + 1 || + expr.arguments.nodes.some(ts.isSpreadElement) ) lowerer.noLowering(`${name} with this argument shape`, expr); - const args = [lowerer.lowerExprExpecting(expr.arguments[0]!, F64)]; - if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments[1]!, arrayOf(BYTES_U8))); - const optional = expr.arguments[required]; + const args = [lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64)]; + if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, arrayOf(BYTES_U8))); + const optional = expr.arguments.nodes[required]; args.push( optional ? lowerBuiltinOptionalDefault(lowerer, optional, F64, numLit(vector ? -1 : 0, loc), vector) @@ -187,7 +187,7 @@ export function lowerFsNumericOpenCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("openSync with numeric flags and this argument shape", expr); } const bits: Record = { @@ -223,17 +223,17 @@ export function lowerFsNumericOpenCall( } return null; }; - const mask = flagsOf(expr.arguments[1]!); + const mask = flagsOf(expr.arguments.nodes[1]!); if (mask === null) { lowerer.noLowering( "openSync with computed numeric flags", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "use an inline bitwise OR of fs.constants.O_RDONLY/O_WRONLY/O_RDWR/O_CREAT/O_EXCL/O_NOFOLLOW/O_NONBLOCK/O_TRUNC/O_APPEND", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const mode = expr.arguments[2] - ? lowerer.lowerExprExpecting(expr.arguments[2]!, F64) + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const mode = expr.arguments.nodes[2] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64) : ({ kind: "numLit", value: 0o666, type: F64, loc } satisfies IrExpr); return { kind: "libCall", @@ -254,24 +254,24 @@ export function lowerFsPromisesOpenCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `fs.promises.open with ${expr.arguments.length} arguments`, + `fs.promises.open with ${expr.arguments.nodes.length} arguments`, expr, "use open(path[, stringFlags[, numericMode]])", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc }; const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc }; - const flags = expr.arguments[1] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[1]!, STRING, defaultFlags) + const flags = expr.arguments.nodes[1] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[1]!, STRING, defaultFlags) : defaultFlags; - const mode = expr.arguments[2] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[2]!, F64, defaultMode) + const mode = expr.arguments.nodes[2] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[2]!, F64, defaultMode) : defaultMode; return { kind: "libCall", @@ -292,16 +292,16 @@ export function lowerFsRenameCallbackCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length !== 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `rename with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `rename with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, "the supported form is rename(oldPath, newPath, callback)", ); } - const oldPath = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const newPath = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); - let callback = lowerer.lowerExpr(expr.arguments[2]!); + const oldPath = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const newPath = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); + let callback = lowerer.lowerExpr(expr.arguments.nodes[2]!); // JS/checkJs callback values may arrive as checked-dynamic callables. // Adapt them to the one error-first slot; the runtime passes a dyn // Error on failure and null on success through the emitted thunk. @@ -310,7 +310,7 @@ export function lowerFsRenameCallbackCall( kind: "dynCheck", value: callback, type: funcOf([DYN], VOID), - loc: locOf(expr.arguments[2]!), + loc: locOf(expr.arguments.nodes[2]!), }; } let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1; @@ -337,7 +337,7 @@ export function lowerFsRenameCallbackCall( if (!callbackOk) { lowerer.unsupported( "SC1090", - expr.arguments[2]!, + expr.arguments.nodes[2]!, "fs.rename callbacks must accept at most one Error | null parameter", ); } @@ -345,7 +345,7 @@ export function lowerFsRenameCallbackCall( // void callback is expected; Node ignores that value. Normalize the // closure to the runtime's void callback ABI after validating its // error-first parameter shape. - callback = voidizedCallback(lowerer, callback, locOf(expr.arguments[2]!)); + callback = voidizedCallback(lowerer, callback, locOf(expr.arguments.nodes[2]!)); return { kind: "libCall", fn: "fs.renameCb", @@ -366,17 +366,17 @@ export function lowerFsReadSyncCall( const supported = "use readSync(fd, buffer), readSync(fd, buffer, { offset?, length?, position? }), or readSync(fd, buffer, offset, length[, position]); bigint positions and non-literal options objects have no lowering"; if ( - expr.arguments.length < 2 || - expr.arguments.length > 5 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > 5 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `readSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `readSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -386,8 +386,8 @@ export function lowerFsReadSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const optionsNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const optionsNode = expr.arguments.nodes[2]!; let valueNode = optionsNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -403,12 +403,12 @@ export function lowerFsReadSyncCall( lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } const args: IrExpr[] = [ - lowerer.lowerExprExpecting(expr.arguments[0]!, F64), - lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), ]; - const positionNode = expr.arguments[4]; + const positionNode = expr.arguments.nodes[4]; let positionValueNode = positionNode; while (positionValueNode && ts.isParenthesizedExpression(positionValueNode)) { positionValueNode = positionValueNode.expression; @@ -441,14 +441,14 @@ export function lowerFsWriteSyncCall( ): IrExpr { const supported = 'use writeSync(fd, buffer[, offset[, length[, position]]]), writeSync(fd, buffer, { offset?, length?, position? }), or writeSync(fd, string[, position[, "utf8"]]); non-literal options objects, bigint positions, and other encodings have no lowering'; - if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2) { + if (expr.arguments.nodes.some(ts.isSpreadElement) || expr.arguments.nodes.length < 2) { lowerer.noLowering( - `writeSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `writeSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - const dataNode = expr.arguments[1]!; + const dataNode = expr.arguments.nodes[1]!; const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const position = (node: ts.Expression | undefined): IrExpr => { let valueNode = node; @@ -474,7 +474,7 @@ export function lowerFsWriteSyncCall( "byte writes take Uint8Array/Buffer data", ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -484,8 +484,8 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const thirdNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const thirdNode = expr.arguments.nodes[2]!; let valueNode = thirdNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -514,37 +514,37 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length !== 4 && expr.arguments.length !== 5) { + if (expr.arguments.nodes.length !== 4 && expr.arguments.nodes.length !== 5) { lowerer.noLowering( - `writeSync of Buffer data with ${expr.arguments.length} arguments`, + `writeSync of Buffer data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeSync", args: [ fd, lowerer.lowerExprExpecting(dataNode, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), - position(expr.arguments[4]), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), + position(expr.arguments.nodes[4]), ], type: F64, loc, }; } if (dataType?.kind === "string") { - if (expr.arguments.length > 4) { + if (expr.arguments.nodes.length > 4) { lowerer.noLowering( - `writeSync of string data with ${expr.arguments.length} arguments`, + `writeSync of string data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const encNode = expr.arguments[3]; + const encNode = expr.arguments.nodes[3]; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode !== undefined) { const encType = lowerer.typeOf(encNode); @@ -559,11 +559,16 @@ export function lowerFsWriteSyncCall( // a call/getter whose type is the accepted literal cannot vanish. enc = lowerer.lowerExprExpecting(encNode, STRING); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeStrSync", - args: [fd, lowerer.lowerExprExpecting(dataNode, STRING), position(expr.arguments[2]), enc], + args: [ + fd, + lowerer.lowerExprExpecting(dataNode, STRING), + position(expr.arguments.nodes[2]), + enc, + ], type: F64, loc, }; @@ -585,22 +590,22 @@ export function lowerFsReadDescriptorCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - if (expr.arguments.length === 1) { + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFdSyncBytes", args: [fd], type: BYTES_U8, loc }; } - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if ( - expr.arguments.length !== 2 || + expr.arguments.nodes.length !== 2 || !(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8")) ) { lowerer.noLowering( `readFileSync(fd) with a non-"utf8" encoding`, - expr.arguments[1] ?? expr, + expr.arguments.nodes[1] ?? expr, 'only utf8 reads are supported: readFileSync(fd, "utf8")', ); } - const enc = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const enc = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc }; } @@ -620,8 +625,9 @@ export function lowerFsEncodedRead( descriptor: boolean, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; - const options = expr.arguments[1]!; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; + const options = expr.arguments.nodes[1]!; const optionsType = lowerer.mapTypeOf(lowerer.typeOf(options)); if (optionsType?.kind !== "string" && optionsType?.kind !== "record") return null; if (optionsType.kind === "record") { @@ -634,7 +640,7 @@ export function lowerFsEncodedRead( return null; } const sourceType = descriptor ? F64 : STRING; - const source = lowerer.lowerExprExpecting(expr.arguments[0]!, sourceType); + const source = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, sourceType); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -674,10 +680,11 @@ export function lowerFsPromisesEncodedRead( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (result?.kind !== "promise" || result.inner.kind !== "string") return null; - const options = expr.arguments[1]!; + const options = expr.arguments.nodes[1]!; const optionsTsType = lowerer.typeOf(options); if ( optionsTsType.isStringLiteralType() && @@ -695,7 +702,7 @@ export function lowerFsPromisesEncodedRead( ) return null; } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -729,15 +736,15 @@ export function lowerFsOpendirCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 2 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 2 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { - lowerer.noLowering(`${member} with ${expr.arguments.length} arguments`, expr); + lowerer.noLowering(`${member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let bufferSize: IrExpr = { kind: "numLit", value: 32, type: F64, loc }; - const optsNode = expr.arguments[1]; + const optsNode = expr.arguments.nodes[1]; if (optsNode && !(ts.isIdentifier(optsNode) && optsNode.text === "undefined")) { const fence = (): never => lowerer.noLowering( @@ -746,7 +753,7 @@ export function lowerFsOpendirCall( "pass an object literal with a numeric bufferSize and a utf8 encoding; recursive walks and other encodings have no lowering", ); if (!ts.isObjectLiteralExpression(optsNode)) fence(); - for (const property of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const property of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const option = optionMember(property); if (!option) fence(); else if (option.name === "bufferSize") @@ -775,7 +782,7 @@ export function lowerFsStreamCreate( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments, + expr.arguments.nodes, member === "createReadStream", `fs.${member}`, expr, @@ -795,7 +802,7 @@ export function lowerFsMkdirSyncCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -805,7 +812,7 @@ export function lowerFsMkdirSyncCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; if (!recursive) { return mode @@ -833,7 +840,7 @@ export function lowerFsPromisesMkdirCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -843,7 +850,7 @@ export function lowerFsPromisesMkdirCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; const type: IrType = { kind: "promise", inner: VOID }; if (!recursive) { @@ -866,18 +873,18 @@ export function lowerFsPromisesMkdirCall( // historical rmOptsSync lowering unchanged. export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const opts = literalBoolOptions( - expr.arguments[1]!, + expr.arguments.nodes[1]!, ["recursive", "force"], ["maxRetries", "retryDelay"], ); if (opts === null) { lowerer.noLowering( "rmSync with an options argument beyond { recursive, force, maxRetries, retryDelay }", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "the recursive/force flags must be boolean literals; maxRetries/retryDelay are numbers; other options have no lowering", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const recursive = boolLit(opts.bools["recursive"] === true, loc); const force = boolLit(opts.bools["force"] === true, loc); @@ -916,7 +923,7 @@ export function lowerFsWriteBytesCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!)); + const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!)); if (dataIr?.kind === "union") { const arms = lowerer.unions.get(dataIr.unionId)?.arms; if ( @@ -930,12 +937,12 @@ export function lowerFsWriteBytesCall( if (dataIr.elem !== "u8") { lowerer.noLowering( `${bi.member} of '${lowerer.fmt(dataIr)}' data`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "byte writes take Uint8Array/Buffer data", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); if (bi.module === "fs/promises") { return { kind: "libCall", @@ -963,7 +970,7 @@ export function lowerFsWriteOptionsCall( loc: SrcLoc, promiseWriteOptions: boolean, ): IrExpr { - const optsNode = expr.arguments[2]!; + const optsNode = expr.arguments.nodes[2]!; const operation = promiseWriteOptions ? "fs.promises.writeFile" : "writeFileSync"; const plainFn: IrLibFn = promiseWriteOptions ? "fsp.writeFile" : "fs.writeFileSync"; const modeFn: IrLibFn = promiseWriteOptions ? "fsp.writeFileMode" : "fs.writeFileModeSync"; @@ -974,8 +981,8 @@ export function lowerFsWriteOptionsCall( // values in object-literal order before issuing the write; otherwise a // call/getter statically typed as the accepted utf8 literal can vanish. const finishWrite = (optionValues: WriteOptionValue[]): IrExpr => { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); const pathLocal = lowerer.declareHiddenLocal("%writePath", STRING); const dataLocal = lowerer.declareHiddenLocal("%writeData", STRING); const stmts: IrStmt[] = [ @@ -1021,7 +1028,7 @@ export function lowerFsWriteOptionsCall( if ( t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8") && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" ) { return finishWrite([{ kind: "effect", value: lowerer.lowerExprExpecting(optsNode, STRING) }]); } @@ -1029,7 +1036,7 @@ export function lowerFsWriteOptionsCall( const optionValues: WriteOptionValue[] = []; let ok = ts.isObjectLiteralExpression(optsNode); if (ok) { - for (const p of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const p of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const m = optionMember(p); if (!m) { ok = false; @@ -1066,7 +1073,7 @@ export function lowerFsWriteOptionsCall( } } } - if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind !== "string") { + if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind !== "string") { lowerer.noLowering( `${operation} with 3 arguments`, optsNode, @@ -1089,20 +1096,20 @@ export function lowerFsReadPathCall( // non-string paths throw ERR_INVALID_ARG_TYPE where the dynCheck // throws its path-annotated TypeError. The fd forms (a number // argument) and literal-utf8 reads keep their existing lowerings. - const pathV = lowerer.lowerExpr(expr.arguments[0]!); + const pathV = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (pathV.type.kind === "string" || pathV.type.kind === "dyn") { const pathArg: IrExpr = pathV.type.kind === "dyn" ? { kind: "dynCheck", value: pathV, type: STRING, loc } : pathV; - if (expr.arguments.length === 1) { + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFileSyncBuf", args: [pathArg], type: BYTES_U8, loc }; } // Runtime nullish and empty encodings read Buffers, supported names // read strings, and invalid names throw ERR_INVALID_ARG_VALUE. A // non-dyn encoding falls through to the literal-utf8 lowering // below (the discarded probe IR never emits). - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if (!(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8"))) { - const enc = lowerer.lowerExpr(expr.arguments[1]!); + const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (enc.type.kind === "dyn") { return { kind: "libCall", @@ -1125,7 +1132,7 @@ function parseMkdirOptions( if (!ts.isObjectLiteralExpression(node)) return null; let recursive = false; let modeNode: ts.Expression | null = null; - for (const property of node.properties) { + for (const property of node.properties.nodes) { const member = optionMember(property); if (!member) return null; if (member.name === "recursive") { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index a091094de..3fbea04aa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -48,8 +48,8 @@ export function lowerFsSyncBufferWindow( source: FsSyncBufferWindowSource, supported: string, ): IrExpr { - const fdValue = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - const bufferValue = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const fdValue = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + const bufferValue = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); const fdLocal = lowerer.declareHiddenLocal("%fsSyncFd", F64); const bufferLocal = lowerer.declareHiddenLocal("%fsSyncBuffer", BYTES_U8); const stmts: IrStmt[] = [ @@ -88,7 +88,7 @@ export function lowerFsSyncBufferWindow( if (!ts.isObjectLiteralExpression(source.node)) { lowerer.noLowering(`${fn} with a non-literal options argument`, source.node, supported); } - for (const property of (source.node as ts.ObjectLiteralExpression).properties) { + for (const property of (source.node as ts.ObjectLiteralExpression).properties.nodes) { const member = optionMember(property); if (member === null) { lowerer.noLowering(`${fn} with a non-plain options member`, property, supported); @@ -189,11 +189,12 @@ export function lowerFsToUnixTimestampCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "fs" || bi.member !== "_toUnixTimestamp") return null; - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return null; + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + return null; return { kind: "libCall", fn: "fs.toUnixTimestamp", - args: [lowerFsTimestampValue(lowerer, expr.arguments[0], loc)], + args: [lowerFsTimestampValue(lowerer, expr.arguments.nodes[0], loc)], type: F64, loc, }; @@ -208,7 +209,7 @@ function opendirLowers(lowerer: Lowerer, args: readonly ts.Expression[]): boolea const options = args[1]; if (!options || (ts.isIdentifier(options) && options.text === "undefined")) return true; if (!ts.isObjectLiteralExpression(options)) return false; - return options.properties.every((property) => { + return options.properties.nodes.every((property) => { const option = optionMember(property); if (!option) return false; if (option.name === "bufferSize") @@ -250,7 +251,7 @@ export function lowerFsLadderCall( } if (bi.module !== "fs" && bi.module !== "fs/promises") return null; if (!isJsSourceFile(expr.getSourceFile())) return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) return null; // Every ladder argument crosses as a dyn value; an argument that // cannot leaves the historical fence in place. @@ -304,12 +305,12 @@ export function lowerFsLadderCall( ts.isIdentifier(pathNode.expression) && pathNode.expression.text === "URL" && lowerer.mapTypeOf(lowerer.typeOf(pathNode))?.kind === "url" && - pathNode.arguments?.length === 1 && - ts.isStringLiteral(pathNode.arguments[0]!) + pathNode.arguments?.nodes.length === 1 && + ts.isStringLiteral(pathNode.arguments.nodes[0]!) ) { let parsed: URL | null = null; try { - parsed = new URL(pathNode.arguments[0]!.text); + parsed = new URL(pathNode.arguments.nodes[0]!.text); } catch { parsed = null; } @@ -512,8 +513,8 @@ export function lowerStringOrBytesWrite( kind: "call", callee: helper, args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, STRING), - lowerer.lowerExprExpecting(call.arguments[1]!, dataType), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, dataType), ], type: VOID, loc, @@ -648,7 +649,8 @@ export function lowerFileHandleMethodCall( }; if (name === "sync" || name === "datasync") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "sync" ? "fileHandle.sync" : "fileHandle.datasync", @@ -658,34 +660,34 @@ export function lowerFileHandleMethodCall( }; } if (name === "truncate" || name === "chmod") { - if (call.arguments.length > 1 || (name === "chmod" && call.arguments.length !== 1)) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || (name === "chmod" && call.arguments.nodes.length !== 1)) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); return { kind: "libCall", fn: name === "truncate" ? "fileHandle.truncate" : "fileHandle.chmod", - args: [receiver(), num(call.arguments[0], 0).value], + args: [receiver(), num(call.arguments.nodes[0], 0).value], type: promise(VOID), loc, }; } if (name === "utimes") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("FileHandle.utimes with this argument shape", call); return { kind: "libCall", fn: "fileHandle.utimes", args: [ receiver(), - lowerFsTimestampValue(lowerer, call.arguments[0], loc), - lowerFsTimestampValue(lowerer, call.arguments[1], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[0], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[1], loc), ], type: promise(VOID), loc, }; } if (name === "readv" || name === "writev") { - if (call.arguments.length < 1 || call.arguments.length > 2) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -694,15 +696,16 @@ export function lowerFileHandleMethodCall( fn: name === "readv" ? "fileHandle.readv" : "fileHandle.writev", args: [ receiver(), - lowerer.lowerExprExpecting(call.arguments[0]!, arrayOf(BYTES_U8)), - num(call.arguments[1], -1).value, + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, arrayOf(BYTES_U8)), + num(call.arguments.nodes[1], -1).value, ], type, loc, }; } if (name === "close" || name === "stat") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "close" ? "fileHandle.close" : "fileHandle.stat", @@ -713,12 +716,12 @@ export function lowerFileHandleMethodCall( } if (name === "readFile") { - if (call.arguments.length > 1) { - lowerer.noLowering(`FileHandle.readFile with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`FileHandle.readFile with ${call.arguments.nodes.length} arguments`, call); } const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise") lowerer.badType(call, lowerer.typeOf(call)); - const encoding = utf8(call.arguments[0]); + const encoding = utf8(call.arguments.nodes[0]); if (typeEquals(type.inner, BYTES_U8)) { return { kind: "libCall", @@ -739,14 +742,14 @@ export function lowerFileHandleMethodCall( } if (name === "writeFile" || name === "appendFile") { - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); // Evaluate a supplied utf8 encoding even for Buffer data, matching // Node's argument order; it does not affect the bytes. - const encoding = utf8(call.arguments[1]); + const encoding = utf8(call.arguments.nodes[1]); if (dataT?.kind === "string") { return { kind: "libCall", @@ -773,10 +776,10 @@ export function lowerFileHandleMethodCall( } if (name === "read" || name === "write") { - const firstNode = call.arguments[0]; + const firstNode = call.arguments.nodes[0]; const firstT = firstNode ? lowerer.mapTypeOf(lowerer.typeOf(firstNode)) : undefined; - const secondT = call.arguments[1] - ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments[1]!)) + const secondT = call.arguments.nodes[1] + ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[1]!)) : undefined; const readOptions = name === "read" && @@ -784,7 +787,7 @@ export function lowerFileHandleMethodCall( const bufferOptions = firstT?.kind === "bytes" && firstT.elem === "u8" && secondT?.kind === "record"; if (readOptions || bufferOptions) { - if (call.arguments.length > (bufferOptions ? 2 : 1)) + if (call.arguments.nodes.length > (bufferOptions ? 2 : 1)) lowerer.noLowering(`FileHandle.${name} options with extra arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") @@ -799,7 +802,7 @@ export function lowerFileHandleMethodCall( let buffer = bufferOptions ? stage(lowerer.lowerExprExpecting(firstNode!, BYTES_U8), "%fhWindowBuffer") : undefined; - const optionsNode = bufferOptions ? call.arguments[1] : firstNode; + const optionsNode = bufferOptions ? call.arguments.nodes[1] : firstNode; const options = optionsNode ? stage(lowerer.lowerExpr(optionsNode), "%fhWindowOptions") : undefined; @@ -858,21 +861,21 @@ export function lowerFileHandleMethodCall( } if (name === "read") { - if (call.arguments.length < 1 || call.arguments.length > 4) { + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { lowerer.noLowering( - `FileHandle.read with ${call.arguments.length} arguments`, + `FileHandle.read with ${call.arguments.nodes.length} arguments`, call, "use read(buffer[, offset[, length[, position]]])", ); } - const buffer = lowerer.lowerExprExpecting(call.arguments[0]!, BYTES_U8); + const buffer = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BYTES_U8); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.read", @@ -883,19 +886,19 @@ export function lowerFileHandleMethodCall( } if (name === "write") { - if (call.arguments.length < 1 || call.arguments.length > 4) { - lowerer.noLowering(`FileHandle.write with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { + lowerer.noLowering(`FileHandle.write with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } if (dataT?.kind === "bytes" && dataT.elem === "u8") { - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.writeBytes", @@ -912,14 +915,14 @@ export function lowerFileHandleMethodCall( }; } if (dataT?.kind === "string") { - if (call.arguments.length > 3) { + if (call.arguments.nodes.length > 3) { lowerer.noLowering( - `FileHandle.write(string) with ${call.arguments.length} arguments`, + `FileHandle.write(string) with ${call.arguments.nodes.length} arguments`, call, 'use write(string[, position[, "utf8"]])', ); } - const position = num(call.arguments[1], -1); + const position = num(call.arguments.nodes[1], -1); return { kind: "libCall", fn: "fileHandle.writeStr", @@ -927,7 +930,7 @@ export function lowerFileHandleMethodCall( receiver(), lowerer.lowerExprExpecting(dataNode, STRING), position.value, - utf8(call.arguments[2]), + utf8(call.arguments.nodes[2]), ], type, loc, @@ -960,7 +963,7 @@ function lowerFileHandleStreamCall( receiver: IrExpr, loc: SrcLoc, ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering(`FileHandle.${name} with ${args.length} arguments`, call); } @@ -1021,7 +1024,7 @@ export function lowerDirMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (DIR_METHODS.has(name) && call.arguments.length === 0) { + if (DIR_METHODS.has(name) && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, DIR_T); if (name === "closeSync") return { kind: "libCall", fn: "dir.closeSync", args: [receiver], type: VOID, loc }; @@ -1093,7 +1096,7 @@ export function lowerStatsMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const fn = Object.hasOwn(STATS_TYPE_TESTS, name) ? STATS_TYPE_TESTS[name] : undefined; - if (fn && call.arguments.length === 0) { + if (fn && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) }; } @@ -1120,9 +1123,9 @@ export function lowerStatsMethodCall( * alive until watcher.close(). */ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fs.watch with this argument shape", @@ -1130,16 +1133,17 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: "the supported forms are watch(path[, options][, listener])", ); } - const hasOptions = expr.arguments.length >= 2 && ts.isObjectLiteralExpression(expr.arguments[1]!); - if (expr.arguments.length === 3 && !hasOptions) { + const hasOptions = + expr.arguments.nodes.length >= 2 && ts.isObjectLiteralExpression(expr.arguments.nodes[1]!); + if (expr.arguments.nodes.length === 3 && !hasOptions) { lowerer.noLowering( "fs.watch with a non-literal options argument", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "pass the options as an object literal: watch(path, { recursive?, persistent?, encoding? }, listener)", ); } if (hasOptions) { - for (const prop of (expr.arguments[1] as ts.ObjectLiteralExpression).properties) { + for (const prop of (expr.arguments.nodes[1] as ts.ObjectLiteralExpression).properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) { lowerer.noLowering( "fs.watch options with computed keys or spreads", @@ -1202,14 +1206,14 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: // An undocumented key, dropped like Node drops it. } } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const args: IrExpr[] = [path]; const listenerArg = hasOptions - ? expr.arguments.length === 3 - ? expr.arguments[2]! + ? expr.arguments.nodes.length === 3 + ? expr.arguments.nodes[2]! : null - : expr.arguments.length === 2 - ? expr.arguments[1]! + : expr.arguments.nodes.length === 2 + ? expr.arguments.nodes[1]! : null; if (listenerArg !== null) { const cb = lowerer.lowerExpr(listenerArg); @@ -1253,7 +1257,7 @@ export function lowerWatcherMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "close" && call.arguments.length === 0) { + if (name === "close" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc }; } @@ -1291,7 +1295,7 @@ export function lowerFsReaddirTypesCall( ): IrExpr { const operation = promiseForm ? "fs.promises.readdir" : "readdirSync"; const spelling = promiseForm ? "readdir" : "readdirSync"; - const optsNode = call.arguments[1]!; + const optsNode = call.arguments.nodes[1]!; if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( `${operation} with a non-literal options argument`, @@ -1300,7 +1304,7 @@ export function lowerFsReaddirTypesCall( ); } let sawWithFileTypes = false; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1372,7 +1376,7 @@ export function lowerFsReaddirTypesCall( ) { fence(); } - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: promiseForm ? "fsp.readdirTypes" : "fs.readdirTypesSync", @@ -1400,8 +1404,8 @@ export function lowerFsStatOptionsCall( ): IrExpr { const spelling = noFollow ? "lstatSync" : "statSync"; const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync"; - const optsNode = call.arguments[1]!; - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const optsNode = call.arguments.nodes[1]!; + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); // The path binds first, then the options' effects, then the stat runs. const plainAfter = (effect: IrExpr | null): IrExpr => { if (effect === null) return { kind: "libCall", fn: plainFn, args: [path], type: STATS_T, loc }; @@ -1481,7 +1485,7 @@ export function lowerFsStatOptionsCall( lowerer.noLowering(`${spelling} with this options value`, optsNode, "pass an object literal"); } } - for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties : []) { + for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties.nodes : []) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1578,7 +1582,7 @@ export function lowerDirentMethodCall( lowerer.checker.getSymbolAtLocation(access.name), ); } - if (call.arguments.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); const dtype: IrExpr = { kind: "recordGet", obj: receiver, diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts index 389da8dc0..f988c204d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts @@ -40,7 +40,7 @@ function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean /** An exclude function receives Node's Dirent objects when withFileTypes is * set, which have no native representation here. */ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: string): void { - const options = call.arguments[1]; + const options = call.arguments.nodes[1]; if (!options) return; const exclude = lowerer.checker.getPropertyOfType(lowerer.typeOf(options), "exclude"); if (!exclude) return; @@ -54,10 +54,10 @@ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: str } function globArgs(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${what} with spread arguments`, call, "write the arguments positionally"); } - return call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + return call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); } /** `globSync(pattern, options?)`: string[] or Dirent[]. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 77ad8fa2e..58ad82358 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -87,7 +87,7 @@ export function lowerFsStreamMethodCall( ): IrExpr | null { if (access.name.text !== "close") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) lowerer.noLowering("close with spread arguments", call); if (args.length > 1) lowerer.noLowering(`close with ${args.length} arguments`, call); const receiver = lowerer.lowerExpr(access.expression); @@ -171,7 +171,7 @@ export function lowerFsStreamNew( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments ?? [], + expr.arguments?.nodes ?? [], cls === "ReadStream", `new fs.${cls}`, expr, diff --git a/packages/compiler/src/frontend/lowering/builtins/ipc.ts b/packages/compiler/src/frontend/lowering/builtins/ipc.ts index 630927a60..cf9d96117 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ipc.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ipc.ts @@ -89,20 +89,20 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr { export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { const loc = locOf(call); if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `process.send with ${call.arguments.length} arguments`, + `process.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "process.send", args: [json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "process.sendCb", args: [json, callback], type: BOOL, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a08b108ad..b89e25f39 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -36,9 +36,9 @@ export function lowerJsonMethodCall( if (member === null) return null; const loc = locOf(call); if ( - (member === "parse" && (call.arguments.length < 1 || call.arguments.length > 2)) || - (member === "stringify" && call.arguments.length > 3) || - call.arguments.some(ts.isSpreadElement) + (member === "parse" && (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2)) || + (member === "stringify" && call.arguments.nodes.length > 3) || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `JSON.${member} with extra or spread arguments`, @@ -47,8 +47,8 @@ export function lowerJsonMethodCall( ); } if (member === "parse") { - const text = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); - const reviver = call.arguments[1]; + const text = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); + const reviver = call.arguments.nodes[1]; if (!reviver || jsonNullishArgument(lowerer, reviver)) { return { kind: "libCall", fn: "json.parse", args: [text], type: DYN, loc }; } @@ -56,8 +56,8 @@ export function lowerJsonMethodCall( 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 replacerNode = call.arguments.nodes[1]; + const spaceNode = call.arguments.nodes[2]; const runtimeOptions = (replacerNode && !jsonNullishArgument(lowerer, replacerNode) && @@ -68,19 +68,19 @@ export function lowerJsonMethodCall( !ts.isStringLiteral(spaceNode) && !ts.isNoSubstitutionTemplateLiteral(spaceNode) && !(ts.isPrefixUnaryExpression(spaceNode) && ts.isNumericLiteral(spaceNode.operand))); - if (runtimeOptions && call.arguments[0]) { + if (runtimeOptions && call.arguments.nodes[0]) { const args = [0, 1, 2].map((index) => - call.arguments[index] - ? lowerer.lowerExprExpecting(call.arguments[index]!, DYN) + call.arguments.nodes[index] + ? lowerer.lowerExprExpecting(call.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: "json.stringifyValue", args, type: DYN, loc }; } const indent = stringifySpaceIndent(lowerer, call); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return lowerer.wrappedUndefined(lowerer.withUndefinedArm(STRING), loc)!; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let value = lowerer.lowerExpr(argNode); // Optional Map/array reads and partially narrowed unions keep their // storage representation when lowered as ordinary arguments. Select @@ -103,7 +103,7 @@ export function lowerJsonMethodCall( if (helper) value = { kind: "call", callee: helper, args: [value], type: narrowed, loc }; } } - const replacer = call.arguments[1]; + const replacer = call.arguments.nodes[1]; if (replacer && !jsonNullishArgument(lowerer, replacer)) { const callback = lowerJsonCallback(lowerer, replacer, "replacer"); if (value.type.kind === "undefinedT" || value.type.kind === "nullT") @@ -573,12 +573,12 @@ function stringifySpaceIndent(lowerer: Lowerer, call: ts.CallExpression): string call, "the serializer is type-directed — shape the value before stringifying", ); - if (call.arguments.length <= 1) return ""; + if (call.arguments.nodes.length <= 1) return ""; const unwrap = (e: ts.Expression): ts.Expression => ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e; const isUndefined = (e: ts.Expression): boolean => jsonNullishArgument(lowerer, e); - if (call.arguments.length === 2) return ""; - const space = unwrap(call.arguments[2]!); + if (call.arguments.nodes.length === 2) return ""; + const space = unwrap(call.arguments.nodes[2]!); if (space.kind === ts.SyntaxKind.NullKeyword || isUndefined(space)) return ""; if (ts.isNumericLiteral(space)) { const n = Number(space.text.replace(/_/g, "")); diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index 72cde522e..ff6c0b38b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -46,13 +46,13 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I const access = call.expression; if (access.questionDotToken || !KEY_OBJECT_METHODS.has(access.name.text)) return null; if (!isKeyObjectValue(lowerer, access.expression)) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; return { kind: "dynInvoke", recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), }; diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 9c3a8c0cf..3a178b126 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -206,7 +206,7 @@ export function builtinNamespaceDestructureModuleOf( module = lowerer.builtinNamespaceModuleOf(init); } if (module === null) return null; - for (const el of decl.name.elements) { + for (const el of decl.name.elements.nodes) { if ( el.dotDotDotToken || el.initializer !== undefined || @@ -231,8 +231,8 @@ function createRequireBaseCallOf(lowerer: Lowerer, expr: ts.Expression): ts.Call if (!ts.isIdentifier(e.expression)) return null; const bi = builtinImportOf(lowerer, e.expression); if (!bi || bi.module !== "module" || bi.member !== "createRequire") return null; - if (e.arguments.length !== 1) return null; - const base = stripTypeCasts(e.arguments[0]!); + if (e.arguments.nodes.length !== 1) return null; + const base = stripTypeCasts(e.arguments.nodes[0]!); if (ts.isIdentifier(base) && lowerer.isStdlibGlobal(base, "__filename")) return e; if ( ts.isPropertyAccessExpression(base) && @@ -310,8 +310,8 @@ export function createRequireSpecOf( if (call.questionDotToken) return null; const baseFile = createRequireCalleeFileOf(lowerer, call.expression); if (baseFile === null) return null; - if (call.arguments.length !== 1) return { spec: null, baseFile }; - const a = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return { spec: null, baseFile }; + const a = call.arguments.nodes[0]!; return { spec: ts.isStringLiteralLike(a) ? a.text : null, baseFile }; } @@ -372,7 +372,7 @@ export function createRequireProgramModuleDecl( if (createRequireProgramModuleOf(lowerer, init) === null) return false; if (ts.isIdentifier(nameNode)) return true; if (!ts.isObjectBindingPattern(nameNode)) return false; - return nameNode.elements.every( + return nameNode.elements.nodes.every( (element) => element.name !== undefined && element.dotDotDotToken === undefined && diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 58ac85aba..956eddb93 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -101,8 +101,8 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "openSync" && - expr.arguments.length >= 2 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "f64" + expr.arguments.nodes.length >= 2 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "f64" ) { return lowerFsNumericOpenCall(lowerer, expr, loc); } @@ -146,7 +146,7 @@ export function lowerBuiltinModuleCall( // verification stance. const syncReaddirTypes = bi.module === "fs" && bi.member === "readdirSync"; const promiseReaddirTypes = bi.module === "fs/promises" && bi.member === "readdir"; - if ((syncReaddirTypes || promiseReaddirTypes) && expr.arguments.length === 2) { + if ((syncReaddirTypes || promiseReaddirTypes) && expr.arguments.nodes.length === 2) { return lowerFsReaddirTypesCall(lowerer, expr, loc, promiseReaddirTypes); } // fs.statSync/fs.lstatSync(path, options): throwIfNoEntry and bigint: @@ -154,7 +154,7 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && (bi.member === "statSync" || bi.member === "lstatSync") && - expr.arguments.length === 2 + expr.arguments.nodes.length === 2 ) { return lowerFsStatOptionsCall(lowerer, expr, loc, bi.member === "lstatSync"); } @@ -216,11 +216,11 @@ export function lowerBuiltinModuleCall( ) { return lowerReadlineCreateCall(lowerer, expr, loc, bi.module === "readline/promises"); } - if (bi.module === "fs" && bi.member === "readFileSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "readFileSync" && expr.arguments.nodes.length === 2) { const encoded = lowerFsEncodedRead( lowerer, expr, - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "f64", + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64", loc, ); if (encoded) return encoded; @@ -233,18 +233,26 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length === 1 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind !== "f64" + expr.arguments.nodes.length === 1 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "f64" ) { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "fs.readFileSyncBytes", args: [path], type: BYTES_U8, loc }; } - if (bi.module === "fs/promises" && bi.member === "readFile" && expr.arguments.length === 2) { + if ( + bi.module === "fs/promises" && + bi.member === "readFile" && + expr.arguments.nodes.length === 2 + ) { const encoded = lowerFsPromisesEncodedRead(lowerer, expr, loc); if (encoded) return encoded; } - if (bi.module === "fs/promises" && bi.member === "readFile" && expr.arguments.length === 1) { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + if ( + bi.module === "fs/promises" && + bi.member === "readFile" && + expr.arguments.nodes.length === 1 + ) { + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "fsp.readFileBytes", @@ -256,8 +264,8 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length >= 1 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "f64" + expr.arguments.nodes.length >= 1 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "f64" ) { return lowerFsReadDescriptorCall(lowerer, expr, loc); } @@ -273,13 +281,13 @@ export function lowerBuiltinModuleCall( ) { return lowerFsStreamCreate(lowerer, expr, bi.member, loc); } - if (bi.module === "fs" && bi.member === "mkdirSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "mkdirSync" && expr.arguments.nodes.length === 2) { return lowerFsMkdirSyncCall(lowerer, expr, loc); } - if (bi.module === "fs/promises" && bi.member === "mkdir" && expr.arguments.length === 2) { + if (bi.module === "fs/promises" && bi.member === "mkdir" && expr.arguments.nodes.length === 2) { return lowerFsPromisesMkdirCall(lowerer, expr, loc); } - if (bi.module === "fs" && bi.member === "rmSync" && expr.arguments.length === 2) { + if (bi.module === "fs" && bi.member === "rmSync" && expr.arguments.nodes.length === 2) { return lowerFsRemoveCall(lowerer, expr, loc); } // accessSync(p, mode?) and truncateSync(p, len?), with their promise @@ -290,20 +298,20 @@ export function lowerBuiltinModuleCall( ? bi.member === "accessSync" || bi.member === "truncateSync" : bi.module === "fs/promises" && (bi.member === "access" || bi.member === "truncate"); if (zeroDefault) { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { - lowerer.noLowering(`${bi.member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { + lowerer.noLowering(`${bi.member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const second: IrExpr = expr.arguments[1] - ? lowerer.lowerExprExpecting(expr.arguments[1], F64) + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const second: IrExpr = expr.arguments.nodes[1] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1], F64) : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: fn.fn, args: [path, second], type: fn.result, loc }; } if ( bi.module === "fs/promises" && bi.member === "appendFile" && - expr.arguments.length === 2 && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "bytes" + expr.arguments.nodes.length === 2 && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "bytes" ) { const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc); if (lowered) return lowered; @@ -311,7 +319,7 @@ export function lowerBuiltinModuleCall( if ( bi.module === "fs" && (bi.member === "writeFileSync" || bi.member === "appendFileSync") && - expr.arguments.length === 2 + expr.arguments.nodes.length === 2 ) { const lowered = lowerFsWriteBytesCall(lowerer, expr, bi, loc); if (lowered) return lowered; @@ -325,15 +333,15 @@ export function lowerBuiltinModuleCall( // remain outside the static surface. const syncWriteOptions = bi.module === "fs" && bi.member === "writeFileSync"; const promiseWriteOptions = bi.module === "fs/promises" && bi.member === "writeFile"; - if ((syncWriteOptions || promiseWriteOptions) && expr.arguments.length === 3) { + if ((syncWriteOptions || promiseWriteOptions) && expr.arguments.nodes.length === 3) { return lowerFsWriteOptionsCall(lowerer, expr, loc, promiseWriteOptions); } if (fn.variadicPack) { // join(...parts) forwards the array itself; mixing spread and plain // arguments (or spreading anything but a string[]) stays out. - const spread = expr.arguments.find(ts.isSpreadElement); + const spread = expr.arguments.nodes.find(ts.isSpreadElement); if (spread) { - if (expr.arguments.length === 1) { + if (expr.arguments.nodes.length === 1) { // The whole-array form forwards the operand directly (no copy). const packed = lowerer.lowerExprExpecting(spread.expression, arrayOf(STRING)); return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; @@ -342,31 +350,31 @@ export function lowerBuiltinModuleCall( // tmpdir.resolve: plain arguments and spread arrays pack into one // fresh string[] (arrayLit's spread positions copy element-wise, // JS-exact; a dyn spread source rides the validated extraction). - const elems = expr.arguments.map((a) => + const elems = expr.arguments.nodes.map((a) => ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(STRING)) : lowerer.lowerExprExpecting(a, STRING), ); - const spreads = expr.arguments.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); + const spreads = expr.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); const packed: IrExpr = { kind: "arrayLit", elems, spreads, type: arrayOf(STRING), loc }; return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } - const elems = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, STRING)); + const elems = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, STRING)); const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(STRING), loc }; return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } if ( bi.module === "fs" && bi.member === "readFileSync" && - expr.arguments.length >= 1 && - expr.arguments.length <= 2 && - !expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length >= 1 && + expr.arguments.nodes.length <= 2 && + !expr.arguments.nodes.some(ts.isSpreadElement) ) { const lowered = lowerFsReadPathCall(lowerer, expr, loc); if (lowered) return lowered; } const required = fn.params.length - (fn.defaults?.length ?? 0); - const hasSpread = expr.arguments.some(ts.isSpreadElement); + const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement); if ( bi.module === "url" && ["fileURLToPath", "fileURLToPathBuffer", "pathToFileURL"].includes(bi.member) @@ -386,10 +394,10 @@ export function lowerBuiltinModuleCall( } if ( !hasSpread && - (expr.arguments.length < required || expr.arguments.length > fn.params.length) + (expr.arguments.nodes.length < required || expr.arguments.nodes.length > fn.params.length) ) { lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, bi.member === "readFileSync" || bi.member === "readFile" ? `pass the encoding: ${bi.member}(path, "utf8") — Buffer reads and options objects have no lowering` @@ -406,11 +414,11 @@ export function lowerBuiltinModuleCall( // "utf8" — or Node's "utf-8" alias, the same decoder (the fallback // declaration enforces the pair at typecheck; @types/node accepts // every BufferEncoding). - const enc = lowerer.typeOf(expr.arguments[1]!); + const enc = lowerer.typeOf(expr.arguments.nodes[1]!); if (!(enc.isStringLiteralType() && (enc.value === "utf8" || enc.value === "utf-8"))) { lowerer.noLowering( `${bi.member} with a non-"utf8" encoding`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, `only utf8 reads are supported: ${bi.member}(path, "utf8")`, ); } @@ -434,7 +442,7 @@ export function lowerBuiltinModuleCall( ); return { type, mode: param.mode === "optional" ? "omittable" : "required" }; }); - const args = lowerer.completeArgs(expr.arguments, shapes, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); return { kind: "callValue", callee, args, type: callee.type.ret, loc }; } let args: IrExpr[]; @@ -453,9 +461,9 @@ export function lowerBuiltinModuleCall( }, }, ); - args = lowerer.completeArgs(expr.arguments, shapes, loc, expr); + args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr); } else { - args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, fn.params[i])); + args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, fn.params[i])); for (let i = args.length; i < fn.params.length; i++) { const dflt = fn.defaults![i - required]!; args.push({ kind: "strLit", value: dflt, type: STRING, loc }); diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index 5fcf35541..afa9dc1ea 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -54,9 +54,9 @@ export function lowerNodeModuleCall( ): IrExpr | null { if (bi.module !== "module") return null; if (bi.member === "syncBuiltinESMExports") { - if (expr.arguments.length !== 0 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 0 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `module.syncBuiltinESMExports with ${expr.arguments.length} arguments`, + `module.syncBuiltinESMExports with ${expr.arguments.nodes.length} arguments`, expr, "syncBuiltinESMExports() takes no arguments", ); @@ -71,14 +71,14 @@ export function lowerNodeModuleCall( return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; } if (bi.member !== "isBuiltin") return null; - if (expr.arguments.length !== 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `module.isBuiltin with ${expr.arguments.length} arguments`, + `module.isBuiltin with ${expr.arguments.nodes.length} arguments`, expr, "isBuiltin(moduleName) takes one string", ); } - const argumentNode = expr.arguments[0]!; + const argumentNode = expr.arguments.nodes[0]!; const argument = lowerer.lowerExpr(argumentNode); if (argument.type.kind !== "string") { if (argument.type.kind === "dyn" || argument.type.kind === "union") { @@ -276,18 +276,18 @@ export function lowerImportMetaResolveCall( ) { return null; } - if (call.questionDotToken !== undefined || call.arguments.length !== 1) { + if (call.questionDotToken !== undefined || call.arguments.nodes.length !== 1) { lowerer.noLowering( "import.meta.resolve with this argument shape", call, 'the lowered form is import.meta.resolve("") using the containing module as its parent', ); } - const specifier = staticString(call.arguments[0]); + const specifier = staticString(call.arguments.nodes[0]); if (specifier === null) { lowerer.noLowering( "import.meta.resolve with a runtime-computed specifier", - call.arguments[0]!, + call.arguments.nodes[0]!, "a compiled binary has a fixed module graph — pass a string literal", ); } @@ -327,7 +327,7 @@ function staticResolveOptionPaths( if (ts.isIdentifier(value) && value.text === "undefined") return undefined; if (!ts.isObjectLiteralExpression(value)) return null; let paths: readonly string[] | undefined; - for (const prop of value.properties) { + for (const prop of value.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; const name = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : null; @@ -335,7 +335,7 @@ function staticResolveOptionPaths( const init = stripTypeCasts(prop.initializer); if (!ts.isArrayLiteralExpression(init)) return null; const entries: string[] = []; - for (const item of init.elements) { + for (const item of init.elements.nodes) { if (!ts.isStringLiteralLike(item)) return null; entries.push(item.text); } @@ -369,8 +369,8 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio if (baseFile === null) return null; if ( call.questionDotToken !== undefined || - call.arguments.length < 1 || - call.arguments.length > (pathsCall ? 1 : 2) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > (pathsCall ? 1 : 2) ) { lowerer.noLowering( pathsCall @@ -379,11 +379,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio call, ); } - const specifier = staticString(call.arguments[0]); + const specifier = staticString(call.arguments.nodes[0]); if (specifier === null) { lowerer.noLowering( `${pathsCall ? "require.resolve.paths" : "require.resolve"} with a runtime-computed request`, - call.arguments[0]!, + call.arguments.nodes[0]!, "a compiled binary has a fixed module graph — pass a string literal", ); } @@ -414,11 +414,11 @@ export function lowerRequireResolveCall(lowerer: Lowerer, call: ts.CallExpressio }; return lowerer.coerceInto(call, raw, lowerer.irTypeOf(call)); } - const paths = staticResolveOptionPaths(call.arguments[1]); + const paths = staticResolveOptionPaths(call.arguments.nodes[1]); if (paths === null) { lowerer.noLowering( "require.resolve with runtime-computed options", - call.arguments[1]!, + call.arguments.nodes[1]!, 'omit options or pass { paths: ["", ...] }', ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts index b49546d8c..d99d63fd3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/net-address.ts +++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts @@ -40,7 +40,7 @@ function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddress export function lowerNetAddressNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const cls = netAddressClassOf(lowerer, expr.expression); if (cls === null) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); const values = args.map((arg) => @@ -93,13 +93,13 @@ const INSTANCE_METHODS: Record> = { export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null; const access = call.expression; - if (access.questionDotToken || call.arguments.some(ts.isSpreadElement)) return null; + if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null; const name = access.name.text; const loc = locOf(call); const statics = Object.hasOwn(STATIC_CALLS, name) ? STATIC_CALLS[name]! : null; if (statics !== null && netAddressClassOf(lowerer, access.expression) === statics.cls) { // The member takes one value; any others still evaluate, in order. - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); const first: IrExpr = args[0] ?? dynUndefinedExpr(loc); const result: IrExpr = { kind: "libCall", @@ -128,7 +128,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression): recv: lowerer.lowerExprExpecting(access.expression, DYN), method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 4f86e6f74..18ca0f1f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -54,10 +54,10 @@ export function lowerNumberStaticCall( const loc = locOf(call); const fn = own(NUMBER_STATIC_PREDICATES, member); if (fn !== undefined) { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Number.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Number.${member} with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const arg = lowerBuiltinValuePreservingUndefined(lowerer, argNode); const optional = lowerOptionalNumberPredicate(lowerer, arg, fn, loc); @@ -73,9 +73,9 @@ export function lowerNumberStaticCall( } if (member === "parseFloat" || member === "parseInt") { const want = member === "parseFloat" ? 1 : 2; - if (call.arguments.length !== want) { + if (call.arguments.nodes.length !== want) { lowerer.noLowering( - `Number.${member} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, + `Number.${member} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, call, member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined, ); @@ -83,9 +83,9 @@ export function lowerNumberStaticCall( // Number.parseFloat/Number.parseInt ARE the global parsers: over a // string argument they lower to the same static parse (parseInt with // its explicit radix); only the coercing argument shapes are refused. - const probed = tryLowerExpression(lowerer, call.arguments[0]!); + const probed = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (probed !== null && isStringLike(lowerer, probed.type)) { - const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc); + const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); if (parsed !== null) return parsed; } // A checked-dynamic argument (`Number.parseInt(env.FORCE_COLOR, 10)` @@ -101,13 +101,13 @@ export function lowerNumberStaticCall( }; if (member === "parseFloat") return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }; - const radix = lowerer.lowerExprExpecting(call.arguments[1]!, F64); + const radix = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } const argText = probed !== null ? lowerer.fmt(probed.type) - : lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(call.arguments[0]!)); + : lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(call.arguments.nodes[0]!)); lowerer.refuseUnsupportedApi( `'Number.${member}' with a '${argText}' argument`, call, diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index b88b97eb6..c10465039 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -28,7 +28,7 @@ export function lowerOsUserInfoCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( "userInfo with options", call, @@ -95,9 +95,9 @@ export function lowerOsNetworkInterfacesCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `networkInterfaces with ${call.arguments.length} arguments`, + `networkInterfaces with ${call.arguments.nodes.length} arguments`, call, "networkInterfaces() takes no arguments", ); @@ -179,9 +179,9 @@ export function lowerOsNetworkInterfacesCall( * record per CPU from the snapshot accessors, in the mapped shapes' own * field order. Other result shapes fence. */ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `cpus with ${call.arguments.length} arguments`, + `cpus with ${call.arguments.nodes.length} arguments`, call, "cpus() takes no arguments", ); @@ -322,8 +322,8 @@ export function lowerPathParseCall( win32: boolean, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length !== 1 || !arg || ts.isSpreadElement(arg)) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !arg || ts.isSpreadElement(arg)) { lowerer.noLowering("path.parse without exactly one path", call, "parse(path) takes one string"); } const result = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -412,8 +412,8 @@ export function lowerPathFormatCall( win32: boolean, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length !== 1 || !arg || ts.isSpreadElement(arg)) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || !arg || ts.isSpreadElement(arg)) { lowerer.noLowering( "path.format without exactly one object", call, @@ -520,7 +520,7 @@ export function lowerOsSetPriorityCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "setPriority with these arguments", @@ -542,8 +542,8 @@ export function lowerOsGetPriorityCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const arg = call.arguments[0]; - if (call.arguments.length > 1 || (arg && ts.isSpreadElement(arg))) { + const arg = call.arguments.nodes[0]; + if (call.arguments.nodes.length > 1 || (arg && ts.isSpreadElement(arg))) { lowerer.noLowering( "getPriority with these arguments", call, diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index f62311f13..2a9962b0f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -83,7 +83,7 @@ export function lowerPerfHooksCall( if (access.questionDotToken) return null; const loc = locOf(expr); if (access.name.text === "now" && isPerfHooksPerformanceExpr(lowerer, access.expression)) { - if (expr.arguments.length !== 0) { + if (expr.arguments.nodes.length !== 0) { lowerer.noLowering( "performance.now with arguments", expr, @@ -99,7 +99,10 @@ export function lowerPerfHooksCall( access.expression.name.text === "now" && isPerfHooksPerformanceExpr(lowerer, access.expression.expression) ) { - if (expr.arguments.length !== 1 || !isPerfHooksPerformanceExpr(lowerer, expr.arguments[0]!)) { + if ( + expr.arguments.nodes.length !== 1 || + !isPerfHooksPerformanceExpr(lowerer, expr.arguments.nodes[0]!) + ) { lowerer.noLowering( "this performance.now.bind form", expr, @@ -118,7 +121,7 @@ export function lowerPerfHooksCall( access.name.text === "toJSON" && isPerfEntryType(lowerer, lowerer.mapTypeOf(lowerer.typeOf(access.expression))) ) { - if (expr.arguments.length !== 0) { + if (expr.arguments.nodes.length !== 0) { lowerer.noLowering("PerformanceEntry toJSON with arguments", expr); } const entry = lowerer.lowerExpr(access.expression); @@ -241,10 +244,10 @@ function perfEntryRecord( * protocol), and a non-iterable spread throws V8's TypeError, so the * runtime always receives an array. */ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - if (!call.arguments.some(ts.isSpreadElement)) { + if (!call.arguments.nodes.some(ts.isSpreadElement)) { return { kind: "dynArrLit", - elems: call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)), + elems: call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)), type: DYN, loc, }; @@ -259,7 +262,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL loc, }, ]; - for (const arg of call.arguments) { + for (const arg of call.arguments.nodes) { if (!ts.isSpreadElement(arg)) { const value = lowerer.lowerExprExpecting(arg, DYN); stmts.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 94c2cdba7..e4e5004c1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -58,13 +58,13 @@ export function lowerProcStreamMethodCall( recv: receiver, method: name, calleeName: call.expression.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; } } - if (name === "getWindowSize" && call.arguments.length === 0) { + if (name === "getWindowSize" && call.arguments.nodes.length === 0) { const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN); return lowerer.coerceInto( call, @@ -80,13 +80,13 @@ export function lowerProcStreamMethodCall( lowerer.irTypeOf(call), ); } - if (name === "write" && call.arguments.length === 1) { + if (name === "write" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExpr(access.expression); - const data = lowerer.lowerExpr(call.arguments[0]!); + const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind !== "string") { lowerer.noLowering( `write of '${lowerer.fmt(data.type)}' data on a stream value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "one string is the supported form here — narrow unions first", ); } @@ -498,14 +498,14 @@ export function lowerProcessMethodCall( if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) { const stream = lowerer.stdlibGlobalMember(access.expression, "process"); if (stream === "stdin" || stream === "stdout" || stream === "stderr") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process stream spread arguments", call); return { kind: "dynInvoke", recv: lowerer.lowerExpr(access.expression), method: access.name.text, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(call), }; @@ -514,31 +514,31 @@ export function lowerProcessMethodCall( const directProcessMember = lowerer.stdlibGlobalMember(access, "process"); if (directProcessMember === "loadEnvFile") return lowerProcessLoadEnvFile(lowerer, call); if (directProcessMember === "hrtime") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process.hrtime with spread arguments", call); const loc = locOf(call); return { kind: "dynCall", callee: { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc }, - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), calleeName: "process.hrtime", type: DYN, loc, }; } if (directProcessMember === "getBuiltinModule") { - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("process.getBuiltinModule with spread arguments", call); const loc = locOf(call); - const id = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + const id = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : (dynUndefinedExpr(loc) satisfies IrExpr); const binding = lowerer.declareHiddenLocal("%builtinId", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: binding.id, init: id, loc }, - ...call.arguments.slice(1).map((arg): IrStmt => ({ + ...call.arguments.nodes.slice(1).map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg), @@ -557,8 +557,8 @@ export function lowerProcessMethodCall( } if (directProcessMember === "send") return lowerProcessIpcSend(lowerer, call); if (directProcessMember === "disconnect") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.disconnect with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.disconnect with ${call.arguments.nodes.length} arguments`, call); } if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( @@ -584,7 +584,7 @@ export function lowerProcessMethodCall( ts.isPropertyAccessExpression(access.expression) && lowerer.stdlibGlobalMember(access.expression, "process") === "stdin" ) { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering("stdin.destroy with arguments", call); } return { kind: "libCall", fn: "process.stdinDestroy", args: [], type: VOID, loc: locOf(call) }; @@ -610,18 +610,18 @@ export function lowerProcessMethodCall( "using the result of stdin.setRawMode(...) (the ReadStream chain — call it as its own statement)", ); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `stdin.setRawMode with ${call.arguments.length} arguments`, + `stdin.setRawMode with ${call.arguments.nodes.length} arguments`, call, "the supported form is setRawMode(mode) with one boolean", ); } - const mode = lowerer.lowerExpr(call.arguments[0]!); + const mode = lowerer.lowerExpr(call.arguments.nodes[0]!); if (mode.type.kind !== "bool") { lowerer.noLowering( `stdin.setRawMode of '${lowerer.fmt(mode.type)}' modes`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the mode is a boolean here — narrow unions first", ); } @@ -639,15 +639,18 @@ export function lowerProcessMethodCall( ) { const loc = locOf(call); const once = access.name.text === "once"; - if (call.arguments.length !== 2) { - lowerer.noLowering(`stdin.${access.name.text} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering( + `stdin.${access.name.text} with ${call.arguments.nodes.length} arguments`, + call, + ); } - const evT = lowerer.typeOf(call.arguments[0]!); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end" && event !== "error" && event !== "keypress") { lowerer.noLowering( `stdin.${access.name.text}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"data", "end", "error", and "keypress" are the supported stdin events (as literals)', ); } @@ -660,15 +663,15 @@ export function lowerProcessMethodCall( } if (event === "keypress") { // readline.emitKeypressEvents' (str, key) events - const listener = stdinKeypressListener(lowerer, call.arguments[1]!); + const listener = stdinKeypressListener(lowerer, call.arguments.nodes[1]!); const onceKp: IrExpr = { kind: "boolLit", value: once, type: BOOL, loc }; return { kind: "libCall", fn: "stdin.onKeypress", args: [listener, onceKp], type: VOID, loc }; } - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "stdin listeners with more than one parameter or a return value", ); } @@ -678,7 +681,7 @@ export function lowerProcessMethodCall( if (param !== undefined && !(param.kind === "bytes" && param.elem === "u8")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `data listeners whose parameter is not 'Uint8Array' (got '${lowerer.fmt(param)}')`, ); } @@ -686,14 +689,18 @@ export function lowerProcessMethodCall( } if (event === "end") { if (param !== undefined) { - lowerer.unsupported("SC1090", call.arguments[1]!, "end listeners with parameters (use ())"); + lowerer.unsupported( + "SC1090", + call.arguments.nodes[1]!, + "end listeners with parameters (use ())", + ); } return { kind: "libCall", fn: "stdin.onEnd", args: [cb, onceArg], type: VOID, loc }; } if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } @@ -703,7 +710,7 @@ export function lowerProcessMethodCall( const stream = lowerer.stdlibGlobalMember(access.expression, "process"); if (stream === "stdout" || stream === "stderr") { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `process.${stream}.write with ${args.length} arguments`, @@ -917,8 +924,11 @@ export function lowerProcessMethodCall( ts.isPropertyAccessExpression(access.expression) && lowerer.stdlibGlobalMember(access.expression, "process") === "memoryUsage" ) { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.memoryUsage.rss with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering( + `process.memoryUsage.rss with ${call.arguments.nodes.length} arguments`, + call, + ); } return { kind: "libCall", fn: "process.rss", args: [], type: F64, loc: locOf(call) }; } @@ -936,10 +946,10 @@ export function lowerProcessMethodCall( // `removeListener` IS `off` — Node aliases them. const isOff = member === "off" || member === "removeListener"; if (member === "on" || member === "once" || isOff) { - if (call.arguments.length !== 2) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } - const evT = lowerer.typeOf(call.arguments[0]!); + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if ((event === "message" || event === "disconnect") && !isOff) { if (!ts.isExpressionStatement(call.parent)) { @@ -951,8 +961,8 @@ export function lowerProcessMethodCall( } const callback = event === "message" - ? ipcMessageListener(lowerer, call.arguments[1]!) - : ipcDisconnectListener(lowerer, call.arguments[1]!); + ? ipcMessageListener(lowerer, call.arguments.nodes[1]!) + : ipcDisconnectListener(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: event === "message" ? "process.onMessage" : "process.onDisconnect", @@ -965,7 +975,7 @@ export function lowerProcessMethodCall( if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported("SC1090", call, "chaining process exception listener registration"); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const monitor = boolLit(event === "uncaughtExceptionMonitor", loc); return { kind: "libCall", @@ -991,7 +1001,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const onceArg: IrExpr = { kind: "boolLit", value: member === "once", type: BOOL, loc }; const fn: IrLibFn = event === "unhandledRejection" @@ -1014,7 +1024,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: isOff ? "process.offWarning" : "process.onWarning", @@ -1027,15 +1037,18 @@ export function lowerProcessMethodCall( if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported("SC1090", call, "chaining process signal listener registration"); } - const name = lowerer.coerceToExpected(lowerer.lowerExpr(call.arguments[0]!), STRING); + const name = lowerer.coerceToExpected(lowerer.lowerExpr(call.arguments.nodes[0]!), STRING); if (name.type.kind !== "string" && name.type.kind !== "dyn") { - lowerer.noLowering("process signal event name that is not a string", call.arguments[0]!); + lowerer.noLowering( + "process signal event name that is not a string", + call.arguments.nodes[0]!, + ); } const signal = name.type.kind === "string" ? name : { kind: "dynCheck" as const, value: name, type: STRING, loc }; - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); return { kind: "libCall", fn: isOff ? "process.offSignal" : "process.onSignal", @@ -1045,7 +1058,7 @@ export function lowerProcessMethodCall( }; } if (event !== "exit") { - lowerer.noLowering(`process.${member}("${event}", ...)`, call.arguments[0]!); + lowerer.noLowering(`process.${member}("${event}", ...)`, call.arguments.nodes[0]!); } if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( @@ -1054,7 +1067,7 @@ export function lowerProcessMethodCall( "chaining process listener registration (the result is void here — register each listener as its own statement)", ); } - let cb = lowerer.lowerExpr(call.arguments[1]!); + let cb = lowerer.lowerExpr(call.arguments.nodes[1]!); // The checked-dynamic listener (test/common's `process.on('exit', // runCallChecks)` — an implicit-any JS function, func(dyn)=>dyn, or // a dyn VALUE that rode an untyped binding): adapt through the dyn @@ -1093,7 +1106,7 @@ export function lowerProcessMethodCall( if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "process listeners with more than one parameter or a return value", ); } @@ -1102,7 +1115,7 @@ export function lowerProcessMethodCall( if (param !== undefined && param.kind !== "f64") { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `exit listeners whose parameter is not 'number' (got '${lowerer.fmt(param)}')`, ); } @@ -1119,19 +1132,19 @@ export function lowerProcessMethodCall( // shape: `.forEach((args) => process.emitWarning(...args))`). if (member === "emitWarning") { let argsArr: IrExpr | null = null; - if (call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!)) { - const spread = lowerer.lowerExpr(call.arguments[0]!.expression); + if (call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!)) { + const spread = lowerer.lowerExpr(call.arguments.nodes[0]!.expression); if (spread.type.kind === "dyn") { argsArr = spread; } else { lowerer.noLowering( "process.emitWarning with a typed spread argument", - call.arguments[0]!, + call.arguments.nodes[0]!, "spread an untyped (checked-dynamic) array, or write the arguments positionally", ); } } else { - argsArr = lowerTracingArguments(lowerer, call.arguments, loc); + argsArr = lowerTracingArguments(lowerer, call.arguments.nodes, loc); } return { kind: "libCall", fn: "process.emitWarning", args: [argsArr], type: VOID, loc }; } @@ -1146,21 +1159,21 @@ export function lowerProcessMethodCall( // through, boxable parameterized shapes ride the checked-dynamic // boundary, trailing call arguments ride the interned dyn thunk. if (member === "nextTick") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { lowerer.noLowering( "process.nextTick with 0 arguments", call, "the supported form is process.nextTick(callback, ...args)", ); } - const cb = timerStyleCallback(lowerer, call.arguments, "process.nextTick", loc); + const cb = timerStyleCallback(lowerer, call.arguments.nodes, "process.nextTick", loc); return { kind: "libCall", fn: "process.nextTick", args: [cb], type: VOID, loc }; } // The process introspection statics — plain reads of the process's // own clocks and counters, Node's shapes exactly. if (member === "uptime" || member === "availableMemory" || member === "constrainedMemory") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } const fn = member === "uptime" @@ -1192,7 +1205,7 @@ export function lowerProcessMethodCall( type: F64, loc, }); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "recordLit", fields: shape.fields.map((f) => ({ name: f.name, value: sampleField(f.name) })), @@ -1200,10 +1213,10 @@ export function lowerProcessMethodCall( loc, }; } - if (call.arguments.length !== 1) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } - const prev = lowerer.lowerExpr(call.arguments[0]!); + const prev = lowerer.lowerExpr(call.arguments.nodes[0]!); const prevShape = prev.type.kind === "record" ? lowerer.shapes.get(prev.type.shapeId) : undefined; const prevOk = @@ -1214,7 +1227,7 @@ export function lowerProcessMethodCall( if (prev.type.kind !== "record" || !prevOk) { lowerer.noLowering( `process.${member} of a '${lowerer.fmt(prev.type)}' previous value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the previous value is the record a prior call answered ({ user, system } numbers) — Node's ERR_INVALID_ARG_TYPE shapes have no lowering", ); } @@ -1282,8 +1295,8 @@ export function lowerProcessMethodCall( // storage lives in the same heap). One sample per call: the interned // helper takes it, then the record reads it field by field. if (member === "memoryUsage") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.memoryUsage with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.memoryUsage with ${call.arguments.nodes.length} arguments`, call); } const t = lowerer.mapTypeOf(lowerer.typeOf(call)); if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -1338,8 +1351,11 @@ export function lowerProcessMethodCall( // process.resourceUsage() — getrusage's 16 fields in Node's names and // units (CPU times in microseconds, maxRSS in kilobytes). if (member === "resourceUsage") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.resourceUsage with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering( + `process.resourceUsage with ${call.arguments.nodes.length} arguments`, + call, + ); } const t = lowerer.mapTypeOf(lowerer.typeOf(call)); if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -1390,9 +1406,9 @@ export function lowerProcessMethodCall( // DIVERGENCE (SEMANTICS.md): resources this runtime does not model // as loop handles (TCP wraps, FS requests) are absent from the answer. if (member === "getActiveResourcesInfo") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering( - `process.getActiveResourcesInfo with ${call.arguments.length} arguments`, + `process.getActiveResourcesInfo with ${call.arguments.nodes.length} arguments`, call, ); } @@ -1402,26 +1418,26 @@ export function lowerProcessMethodCall( // completes it to the -1 read sentinel); umask(mask) sets and answers // the previous mask, Node's shape either way. if (member === "umask") { - if (call.arguments.length > 1) { - lowerer.noLowering(`process.umask with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`process.umask with ${call.arguments.nodes.length} arguments`, call); } const mask: IrExpr = - call.arguments.length === 1 - ? lowerer.lowerExpr(call.arguments[0]!) + call.arguments.nodes.length === 1 + ? lowerer.lowerExpr(call.arguments.nodes[0]!) : { kind: "numLit", value: -1, type: F64, loc }; if (mask.type.kind !== "f64") { - lowerer.noLowering("process.umask of non-number masks", call.arguments[0]!); + lowerer.noLowering("process.umask of non-number masks", call.arguments.nodes[0]!); } return { kind: "libCall", fn: "process.umask", args: [mask], type: F64, loc }; } // chdir(2) — throws Node's fs-shaped error on failure. if (member === "chdir") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`process.chdir with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`process.chdir with ${call.arguments.nodes.length} arguments`, call); } - const dir = lowerer.lowerExpr(call.arguments[0]!); + const dir = lowerer.lowerExpr(call.arguments.nodes[0]!); if (dir.type.kind !== "string") { - lowerer.noLowering("process.chdir of non-string paths", call.arguments[0]!); + lowerer.noLowering("process.chdir of non-string paths", call.arguments.nodes[0]!); } return { kind: "libCall", fn: "process.chdir", args: [dir], type: VOID, loc }; } @@ -1430,8 +1446,8 @@ export function lowerProcessMethodCall( // the member optional (that optionality covers Windows). The `?.()` // spelling routes here through lowerProcessOptionalMethodCall. if (PROCESS_CREDENTIAL_FNS.has(member)) { - if (call.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${call.arguments.nodes.length} arguments`, call); } return processCredentialCall(member, loc); } @@ -1443,11 +1459,11 @@ export function lowerProcessMethodCall( // Node's `kill ESRCH`/`kill EPERM` Error. The result is Node's // constant true. if (member === "kill") { - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`process.kill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`process.kill with ${call.arguments.nodes.length} arguments`, call); } - const pid = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const sigNode = call.arguments[1]; + const pid = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const sigNode = call.arguments.nodes[1]; if (!sigNode) { const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc }; return { kind: "libCall", fn: "process.kill", args: [pid, dflt], type: BOOL, loc }; @@ -1466,7 +1482,7 @@ export function lowerProcessMethodCall( ); } if (member === "exit") { - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; const code: IrExpr = arg !== undefined ? lowerer.lowerExprExpecting(arg, F64) @@ -1491,8 +1507,8 @@ export function lowerProcessOptionalMethodCall( const member = lowerer.stdlibGlobalMember(expr.expression, "process"); if (member === "send") return lowerProcessIpcSend(lowerer, expr); if (member === null || !PROCESS_CREDENTIAL_FNS.has(member)) return null; - if (expr.arguments.length !== 0) { - lowerer.noLowering(`process.${member} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) { + lowerer.noLowering(`process.${member} with ${expr.arguments.nodes.length} arguments`, expr); } return processCredentialCall(member, locOf(expr)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index da680a86e..2656da18b 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -37,12 +37,12 @@ export function lowerPromiseStaticCall( if (member === null) return null; const loc = locOf(call); if (member === "race") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if ( !argNode || !ts.isArrayLiteralExpression(argNode) || - argNode.elements.some(ts.isSpreadElement) || - argNode.elements.length === 0 + argNode.elements.nodes.some(ts.isSpreadElement) || + argNode.elements.nodes.length === 0 ) { lowerer.noLowering( "Promise.race over this argument shape", @@ -71,7 +71,7 @@ export function lowerPromiseStaticCall( } return resultArms.some((a) => typeEquals(a, inner)); }; - const entries = argNode.elements.map((el) => { + const entries = argNode.elements.nodes.map((el) => { const entry = lowerer.lowerExpr(el); if (entry.type.kind !== "promise") { lowerer.noLowering( @@ -106,7 +106,7 @@ export function lowerPromiseStaticCall( // Heterogeneous ARRAY LITERALS land on the tuple overload // (Promise<[A, B]>) and fence here — one promise type is the bound. if (member === "all") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if (!argNode) { lowerer.noLowering( "Promise.all with this argument shape", @@ -123,10 +123,10 @@ export function lowerPromiseStaticCall( // like the tuple; the elements are the same T either way). if ( ts.isArrayLiteralExpression(argNode) && - !argNode.elements.some(ts.isSpreadElement) && - argNode.elements.length > 0 + !argNode.elements.nodes.some(ts.isSpreadElement) && + argNode.elements.nodes.length > 0 ) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]!.type; if ( first.kind === "promise" && @@ -213,7 +213,7 @@ export function lowerPromiseStaticCall( return { kind: "intrinsic", name: "promise.all", args: [entries], type: resultT, loc }; } if (member === "allSettled" || member === "any") { - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if (!argNode || ts.isSpreadElement(argNode)) { lowerer.noLowering( `Promise.${member} with this argument shape`, @@ -224,8 +224,11 @@ export function lowerPromiseStaticCall( if (isJsSourceFile(call.getSourceFile())) { // JavaScript entries are checked-dynamic: any iterable, thenables // and plain values included, exactly like Promise.all. - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + if ( + ts.isArrayLiteralExpression(argNode) && + !argNode.elements.nodes.some(ts.isSpreadElement) + ) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); if (elems.every((entry) => lowerer.dynConvertible(entry.type))) { const entries: IrExpr = { kind: "arrayLit", @@ -258,7 +261,7 @@ export function lowerPromiseStaticCall( // (plain-value resolve, Error-pinned reject); the emitter assembles // the record from the newPromise machinery. if (member === "withResolvers") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Promise.withResolvers with arguments`, call); } // The type mapper owns the record shape (its withResolvers @@ -284,11 +287,11 @@ export function lowerPromiseStaticCall( // types. `Promise.try(f, ...args)` evaluates the arguments at the call // and passes them to f, which still runs synchronously. if (member === "try") { - const fNode = call.arguments[0]; - const argNodes = call.arguments.slice(1); - if (fNode === undefined || call.arguments.some(ts.isSpreadElement)) { + const fNode = call.arguments.nodes[0]; + const argNodes = call.arguments.nodes.slice(1); + if (fNode === undefined || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `Promise.try with ${call.arguments.length === 0 ? "no" : "spread"} arguments`, + `Promise.try with ${call.arguments.nodes.length === 0 ? "no" : "spread"} arguments`, call, "pass the callback and its arguments directly: Promise.try(f, a, b)", ); @@ -391,10 +394,13 @@ export function lowerPromiseStaticCall( // object would be ADOPTED in JS, not wrapped) and promise-armed // unions (wrap-or-identity depends on the runtime arm) fence. if (member === "resolve") { - if (call.arguments.length > 1 || call.arguments.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering(`Promise.resolve with ${call.arguments.length} arguments`, call); + if ( + call.arguments.nodes.length > 1 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`Promise.resolve with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (argNode !== undefined) { const argT = lowerer.typeOf(argNode); const argIr = lowerer.mapTypeOf(argT); @@ -502,10 +508,10 @@ function typedPromiseEntries( entries = lowered; } else if ( ts.isArrayLiteralExpression(argNode) && - !argNode.elements.some(ts.isSpreadElement) && - argNode.elements.length > 0 + !argNode.elements.nodes.some(ts.isSpreadElement) && + argNode.elements.nodes.length > 0 ) { - const elems = argNode.elements.map((el) => lowerer.lowerExpr(el)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]!.type; if ( first.kind !== "promise" || diff --git a/packages/compiler/src/frontend/lowering/builtins/querystring.ts b/packages/compiler/src/frontend/lowering/builtins/querystring.ts index 34ef9c55b..0000fd44e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/querystring.ts +++ b/packages/compiler/src/frontend/lowering/builtins/querystring.ts @@ -51,8 +51,8 @@ export function lowerQuerystringParseCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length > 4 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`querystring.parse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 4 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`querystring.parse with ${call.arguments.nodes.length} arguments`, call); } const fence: () => never = () => lowerer.noLowering( @@ -82,17 +82,17 @@ export function lowerQuerystringParseCall( else if (arm.kind !== "undefinedT" && arm.kind !== "f64") fence(); } if (!sawStr || !sawArr) fence(); - const argument = call.arguments[0]; + const argument = call.arguments.nodes[0]; if (!argument) lowerer.noLowering("querystring.parse without a query string", call); const str = lowerer.lowerExprExpecting(argument, STRING); - const sep = qsSepEqArg(lowerer, call.arguments[1], "&", "querystring.parse", loc); - const eq = qsSepEqArg(lowerer, call.arguments[2], "=", "querystring.parse", loc); + const sep = qsSepEqArg(lowerer, call.arguments.nodes[1], "&", "querystring.parse", loc); + const eq = qsSepEqArg(lowerer, call.arguments.nodes[2], "=", "querystring.parse", loc); // The options walk: maxKeys lowers (Node's rule — > 0 caps the pair // count, 0 and negatives mean unlimited — lives in the runtime, so // any number expression works); a custom decodeURIComponent changes // every decoded byte and fences by name. let maxKeys: IrExpr = { kind: "numLit", value: 1000, type: F64, loc }; - const optsNode = call.arguments[3]; + const optsNode = call.arguments.nodes[3]; if (optsNode) { if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( @@ -101,7 +101,7 @@ export function lowerQuerystringParseCall( "the supported form spells the options inline: parse(s, sep, eq, { maxKeys: n })", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -144,10 +144,10 @@ export function lowerQuerystringStringifyCall( call: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (call.arguments.length > 4 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`querystring.stringify with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 4 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`querystring.stringify with ${call.arguments.nodes.length} arguments`, call); } - const objNode = call.arguments[0]; + const objNode = call.arguments.nodes[0]; // stringify() / stringify(undefined) / stringify(null): Node answers // '' for every non-object — the constant folds. if ( @@ -170,9 +170,9 @@ export function lowerQuerystringStringifyCall( "pass a record of string/number/boolean values (arrays of those expand to repeated keys; null/undefined values serialize empty)", ); } - const sep = qsSepEqArg(lowerer, call.arguments[1], "&", "querystring.stringify", loc); - const eq = qsSepEqArg(lowerer, call.arguments[2], "=", "querystring.stringify", loc); - const optsNode = call.arguments[3]; + const sep = qsSepEqArg(lowerer, call.arguments.nodes[1], "&", "querystring.stringify", loc); + const eq = qsSepEqArg(lowerer, call.arguments.nodes[2], "=", "querystring.stringify", loc); + const optsNode = call.arguments.nodes[3]; if (optsNode) { if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( @@ -181,7 +181,7 @@ export function lowerQuerystringStringifyCall( "the supported form spells the options inline (and the only documented option, encodeURIComponent, has no lowering)", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index a14f15f9e..3a0e7ccf0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -88,7 +88,7 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E ); } let signal: ts.Expression | null = null; - for (const p of node.properties) { + for (const p of node.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "signal") { signal = p.initializer; continue; @@ -129,7 +129,7 @@ function processOutputOf(lowerer: Lowerer, node: ts.Expression): 0 | 1 | 2 { export function lowerReadlineUiNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!isReadlineUiTyped(lowerer, expr)) return null; const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new Readline with ${args.length} arguments`, expr); } @@ -158,7 +158,7 @@ export function lowerReadlineUiNew(lowerer: Lowerer, expr: ts.NewExpression): Ir "write the options inline: new Readline(stream, { autoCommit: true })", ); } - for (const p of opts.properties) { + for (const p of opts.properties.nodes) { if ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -199,7 +199,7 @@ function lowerReadlineUiCall( ): IrExpr { const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const plan: Record = { cursorTo: ["rl.uiCursorTo", 1, 2], moveCursor: ["rl.uiMoveCursor", 2, 2], @@ -294,7 +294,7 @@ export function lowerReadlineMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64); if ( name === "question" && @@ -521,7 +521,7 @@ function lowerWriteKey( ); } hasKey = true; - for (const p of keyNode.properties) { + for (const p of keyNode.properties.nodes) { const member = ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) ? p.name.text : null; const value = member !== null ? (p as ts.PropertyAssignment).initializer : null; if (member === "name" && value !== null && !isNullish(value)) { @@ -568,7 +568,7 @@ function lowerWriteKey( * take the interface handle (0 for none) and the timeout (NaN: the * interface's, or 500). */ function lowerEmitKeypressEvents(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`readline.emitKeypressEvents with ${args.length} arguments`, expr); } @@ -601,7 +601,7 @@ function lowerEmitKeypressEvents(lowerer: Lowerer, expr: ts.CallExpression, loc: if (isReadlineTyped(lowerer, ifaceNode)) { iface = lowerer.lowerExprExpecting(ifaceNode, F64); } else if (ts.isObjectLiteralExpression(ifaceNode)) { - for (const p of ifaceNode.properties) { + for (const p of ifaceNode.properties.nodes) { if ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -708,7 +708,7 @@ export function lowerReadlineCursorCall( if (member === "emitKeypressEvents") return lowerEmitKeypressEvents(lowerer, expr, loc); const kind = CURSOR_HELPERS[member]; if (kind === undefined) return null; - const args = [...expr.arguments]; + const args = [...expr.arguments.nodes]; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`readline.${member} with a spread argument`, expr); } @@ -983,7 +983,7 @@ export function lowerReadlineCreateCall( }); terminal = { kind: "varRef", localId: local.id, type: BOOL, loc }; }; - const optsNode = expr.arguments[0]; + const optsNode = expr.arguments.nodes[0]; if (optsNode === undefined) { lowerer.noLowering( "createInterface without arguments", @@ -993,33 +993,34 @@ export function lowerReadlineCreateCall( } if (!ts.isObjectLiteralExpression(optsNode)) { // createInterface(input, output?, completer?, terminal?) - if (expr.arguments.length > 4) { + if (expr.arguments.nodes.length > 4) { lowerer.noLowering( - `createInterface with ${expr.arguments.length} arguments`, - expr.arguments[4]!, + `createInterface with ${expr.arguments.nodes.length} arguments`, + expr.arguments.nodes[4]!, "pass createInterface(input, output, completer, terminal) or one options object", ); } input = lowerInput(lowerer, optsNode); - if (expr.arguments[1] !== undefined) output = lowerOutput(lowerer, expr.arguments[1]); - const completerNode = expr.arguments[2]; + if (expr.arguments.nodes[1] !== undefined) + output = lowerOutput(lowerer, expr.arguments.nodes[1]); + const completerNode = expr.arguments.nodes[2]; if ( completerNode !== undefined && !(ts.isIdentifier(completerNode) && completerNode.text === "undefined") ) { setCompleter(completerNode); } - if (expr.arguments[3] !== undefined) setTerminal(expr.arguments[3]); + if (expr.arguments.nodes[3] !== undefined) setTerminal(expr.arguments.nodes[3]); } else { - if (expr.arguments.length > 1) { + if (expr.arguments.nodes.length > 1) { lowerer.noLowering( "createInterface with more than one argument", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "pass every option in the one options object", ); } let sawInput = false; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const shorthand = ts.isShorthandPropertyAssignment(p); if (!(shorthand || (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)))) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8b951e96c..1cf547be9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -23,13 +23,13 @@ export function lowerReflectCall( !call.questionDotToken && !access.questionDotToken && lowerer.stdlibGlobalMember(access, "Reflect") === "ownKeys" && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { return { kind: "libCall", fn: "dyn.ownKeys", - args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], type: DYN, loc: locOf(call), }; @@ -39,9 +39,9 @@ export function lowerReflectCall( if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( - call.arguments.length < required || - call.arguments.length > required + 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < required || + call.arguments.nodes.length > required + 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Reflect.${reflectMember} with this argument shape`, @@ -50,9 +50,11 @@ export function lowerReflectCall( ); } const loc = locOf(call); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); const target = - call.arguments.length === required ? lowerer.declareHiddenLocal("%reflectTarget", DYN) : null; + call.arguments.nodes.length === required + ? lowerer.declareHiddenLocal("%reflectTarget", DYN) + : null; const operation: IrExpr = { kind: "libCall", fn: reflectMember === "get" ? "dyn.reflectGet" : "dyn.reflectSet", @@ -76,16 +78,16 @@ export function lowerReflectCall( const loc = locOf(call); const fenceHint = "the lowered form is Reflect.apply(path.join | path.resolve, , args) — call other functions directly (f(...args))"; - if (call.arguments.length !== 3 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("Reflect.apply with this argument shape", call, fenceHint); } - const targetNode = call.arguments[0]!; + const targetNode = call.arguments.nodes[0]!; const bi = ts.isPropertyAccessExpression(targetNode) ? lowerer.builtinMemberOf(targetNode) : null; const fn = bi ? builtinModuleFnOf(lowerer, bi.module, bi.member) : null; if (!fn || fn.variadicPack !== true) { const target = lowerer.lowerExprExpecting(targetNode, DYN); - const receiver = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); - const argumentsList = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + const receiver = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); + const argumentsList = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN); return { kind: "libCall", fn: "dyn.reflectApply", @@ -94,7 +96,7 @@ export function lowerReflectCall( loc, }; } - const thisNode = call.arguments[1]!; + const thisNode = call.arguments.nodes[1]!; const effectFree = thisNode.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(thisNode) || @@ -107,6 +109,6 @@ export function lowerReflectCall( "the target ignores its receiver, so only effect-free spellings drop honestly (`this`, a binding, null, undefined)", ); } - const packed = lowerer.lowerExprExpecting(call.arguments[2]!, arrayOf(STRING)); + const packed = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, arrayOf(STRING)); return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index b117c24ff..013cd3fda 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -29,13 +29,16 @@ export function lowerStringStaticCall( // literal, the fromCharCode convention. The runtime interleaves per // the spec's loop. if (member === "raw") { - if (call.arguments.length < 1 || call.arguments.some((a) => ts.isSpreadElement(a))) { + if ( + call.arguments.nodes.length < 1 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { lowerer.noLowering( - `String.raw with ${call.arguments.length === 0 ? "no template" : "spread substitutions"}`, + `String.raw with ${call.arguments.nodes.length === 0 ? "no template" : "spread substitutions"}`, call, ); } - const tmplNode = call.arguments[0]!; + const tmplNode = call.arguments.nodes[0]!; const tmpl = lowerer.lowerExpr(tmplNode); const rawT = arrayOf(STRING); let raw: IrExpr | null = null; @@ -60,7 +63,7 @@ export function lowerStringStaticCall( "the template must carry a string[] `raw` member: String.raw({ raw: [...] }, ...subs)", ); } - const subs = call.arguments.slice(1).map((a): IrExpr => { + const subs = call.arguments.nodes.slice(1).map((a): IrExpr => { const v = lowerer.lowerExpr(a); if (isUnitType(v.type)) return lowerer.ensureString(v, a); if (v.type.kind === "string") return v; @@ -76,9 +79,9 @@ export function lowerStringStaticCall( const packedSubs: IrExpr = { kind: "arrayLit", elems: subs, type: rawT, loc }; return { kind: "libCall", fn: "string.raw", args: [raw, packedSubs], type: STRING, loc }; } - const spread = call.arguments.find(ts.isSpreadElement); + const spread = call.arguments.nodes.find(ts.isSpreadElement); if (spread) { - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( `String.${member} with a mixed spread call`, call, @@ -100,7 +103,7 @@ export function lowerStringStaticCall( } // Each code converts with ToNumber: a missing value is NaN (code unit 0 // for fromCharCode, a RangeError for fromCodePoint), exactly Node. - const elems = call.arguments.map((a) => lowerToNumberArgument(lowerer, a)); + const elems = call.arguments.nodes.map((a) => lowerToNumberArgument(lowerer, a)); const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc }; } @@ -119,7 +122,7 @@ export function lowerStringLastIndexOfCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "string") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "lastIndexOf with this argument shape", call, @@ -127,12 +130,12 @@ export function lowerStringLastIndexOfCall( ); } const receiver = lowerer.lowerExprExpecting(access.expression, STRING); - const positionNode = call.arguments[1]; + const positionNode = call.arguments.nodes[1]; if (!positionNode) { - const needle = lowerStringSearchArgument(lowerer, call.arguments[0], loc); + const needle = lowerStringSearchArgument(lowerer, call.arguments.nodes[0], loc); return { kind: "libCall", fn: "string.lastIndexOf", args: [receiver, needle], type: F64, loc }; } - const needle = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const needle = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); const position = lowerOptionalArgument( lowerer, positionNode, diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 3adf1a3b6..a5f34f44e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -37,9 +37,9 @@ export function lowerStringDecoderMethodCall( const loc = locOf(call); if ( (name !== "write" && name !== "end") || - call.arguments.length > 1 || - (name === "write" && call.arguments.length !== 1) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + (name === "write" && call.arguments.nodes.length !== 1) || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `StringDecoder.${name}`, @@ -51,7 +51,7 @@ export function lowerStringDecoderMethodCall( const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "record") lowerer.badType(access.expression, lowerer.typeOf(access.expression)); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "call", callee: stringDecoderHelper(lowerer, "end", receiver.type.shapeId, loc), @@ -60,7 +60,7 @@ export function lowerStringDecoderMethodCall( loc, }; } - const node = call.arguments[0]!; + const node = call.arguments.nodes[0]!; const undefinedArg = name === "end" ? lowerStaticallyUndefinedArgument(lowerer, node) : null; if (undefinedArg) { const saved = lowerer.declareHiddenLocal("%strdecRecv", receiver.type); @@ -287,7 +287,7 @@ export function lowerTextCodecNew( ctor: ts.NewExpression, cls: TextCodecCtor["cls"], ): IrExpr { - const args = ctor.arguments ?? []; + const args = ctor.arguments?.nodes ?? []; const loc = locOf(ctor); let encoding: IrExpr = { kind: "numLit", value: -1, type: F64, loc }; let label: IrExpr | null = null; @@ -317,7 +317,7 @@ export function lowerTextCodecNew( if (options) { if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder options outside an object literal", options); - for (const property of options.properties) { + for (const property of options.properties.nodes) { if ( (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || !ts.isIdentifier(property.name) || @@ -380,16 +380,16 @@ function lowerStoredTextCodecCall( const cls = storedTextCodecClassOf(lowerer, access.expression); if (cls === null || !lowerer.isStdlibMember(access)) return null; if (access.name.text !== (cls === "TextEncoder" ? "encode" : "decode")) return null; - if (call.arguments.length > (cls === "TextDecoder" ? 2 : 1)) { + if (call.arguments.nodes.length > (cls === "TextDecoder" ? 2 : 1)) { lowerer.noLowering(`${cls}.${access.name.text} with extra arguments`, call); } const loc = locOf(call); let stream: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc }; - if (cls === "TextDecoder" && call.arguments[1]) { - const options = stripTypeCasts(call.arguments[1]); + if (cls === "TextDecoder" && call.arguments.nodes[1]) { + const options = stripTypeCasts(call.arguments.nodes[1]); if (!ts.isObjectLiteralExpression(options)) lowerer.noLowering("TextDecoder.decode options outside an object literal", options); - for (const property of options.properties) { + for (const property of options.properties.nodes) { if ( (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) || !ts.isIdentifier(property.name) || @@ -406,13 +406,13 @@ function lowerStoredTextCodecCall( if (receiver.type.kind !== "record") lowerer.badType(access.expression, lowerer.typeOf(access.expression)); let arg: IrExpr = - call.arguments.length === 0 + call.arguments.nodes.length === 0 ? cls === "TextEncoder" ? strLit("", loc) : { kind: "bytesNew", source: null, type: BYTES_U8, loc } : cls === "TextEncoder" - ? lowerer.lowerExprExpecting(call.arguments[0]!, STRING) - : lowerer.lowerExpr(call.arguments[0]!); + ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING) + : lowerer.lowerExpr(call.arguments.nodes[0]!); if ( cls === "TextDecoder" && (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) @@ -420,7 +420,7 @@ function lowerStoredTextCodecCall( arg = { kind: "libCall", fn: "bytes.bufferSource", - args: [lowerer.coerceInto(call.arguments[0]!, arg, DYN)], + args: [lowerer.coerceInto(call.arguments.nodes[0]!, arg, DYN)], type: BYTES_U8, loc, }; @@ -542,14 +542,14 @@ export function lowerTextCodecCall( storedTextCodecClassOf(lowerer, access.expression) === "TextEncoder" && lowerer.isStdlibMember(access) ) { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("TextEncoder.encodeInto with this argument shape", call); const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const result: IrExpr = { kind: "libCall", fn: "text.encodeInto", - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(stripTypeCasts(arg), DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(stripTypeCasts(arg), DYN)), type: DYN, loc, }; @@ -569,7 +569,10 @@ export function lowerTextCodecCall( if (info === null) return lowerStoredTextCodecCall(lowerer, call, access); if (!lowerer.isStdlibMember(access)) return null; const { cls, ctor: recv } = info; - if (cls === "TextDecoder" && ((recv.arguments?.length ?? 0) > 1 || call.arguments.length > 1)) + if ( + cls === "TextDecoder" && + ((recv.arguments?.nodes.length ?? 0) > 1 || call.arguments.nodes.length > 1) + ) return lowerStoredTextCodecCall(lowerer, call, access); if ( !(cls === "TextDecoder" && member === "decode") && @@ -578,7 +581,7 @@ export function lowerTextCodecCall( return null; } const loc = locOf(call); - const ctorArgs = recv.arguments ?? []; + const ctorArgs = recv.arguments?.nodes ?? []; if (cls === "TextDecoder") { // The label must be statically known. A literal-typed effectful inline // expression is accepted, but its evaluation is sequenced before the @@ -605,18 +608,18 @@ export function lowerTextCodecCall( type: result.type, loc, }; - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { // decode() with no input is "" per spec — nothing to evaluate. return afterLabel({ kind: "strLit", value: "", type: STRING, loc }); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( "decode with a stream option", call, "streaming decode has no lowering — decode whole buffers", ); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let arg = lowerer.lowerExpr(argNode); if (arg.type.kind === "dyn" || (arg.type.kind === "bytes" && arg.type.elem !== "u8")) { arg = { @@ -649,13 +652,13 @@ export function lowerTextCodecCall( if (ctorArgs.length > 0) { lowerer.noLowering("new TextEncoder with arguments", recv); } - if (call.arguments.length > 1) { - lowerer.noLowering(`TextEncoder.encode with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`TextEncoder.encode with ${call.arguments.nodes.length} arguments`, call); } const s = - call.arguments.length === 0 + call.arguments.nodes.length === 0 ? strLit("", loc) - : lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + : lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); const enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; return { kind: "libCall", fn: "buffer.fromStr", args: [s, enc], type: BYTES_U8, loc }; } @@ -667,15 +670,15 @@ export function lowerBufferEncodingCall( fn: BuiltinModuleFn, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("buffer encoding call with spread arguments", expr); const arity = bi.member === "transcode" ? 3 : 1; const args = Array.from({ length: arity }, (_, index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(stripTypeCasts(expr.arguments[index]!), DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(stripTypeCasts(expr.arguments.nodes[index]!), DYN) : dynUndefinedExpr(loc), ); - if (expr.arguments.length > arity) + if (expr.arguments.nodes.length > arity) lowerer.noLowering("buffer encoding call with extra arguments", expr); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 98624dc4a..dabfb1d2a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -64,18 +64,18 @@ export function lowerTimersPromisesSetInterval( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "timers/promises" || bi.member !== "setInterval") return null; - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", expr, "spread arguments"); } - if (expr.arguments.length !== 2) { + if (expr.arguments.nodes.length !== 2) { lowerer.noLowering( - `timers/promises.setInterval with ${expr.arguments.length} arguments`, + `timers/promises.setInterval with ${expr.arguments.nodes.length} arguments`, expr, "the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet", ); } - const delayNode = expr.arguments[0]!; - const valueNode = expr.arguments[1]!; + const delayNode = expr.arguments.nodes[0]!; + const valueNode = expr.arguments.nodes[1]!; const delay = lowerer.lowerExpr(delayNode); const ms = delay.kind === "unitLit" @@ -184,7 +184,7 @@ export function lowerTimeoutMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if ((name === "unref" || name === "ref") && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) { const handle = lowerer.lowerExprExpecting(access.expression, F64); const fn = isImmediate ? name === "unref" @@ -197,12 +197,12 @@ export function lowerTimeoutMethodCall( // returns the Timeout/Immediate); the libCall yields the f64 back. return { kind: "libCall", fn, args: [handle], type: F64, loc }; } - if (name === "hasRef" && call.arguments.length === 0) { + if (name === "hasRef" && call.arguments.nodes.length === 0) { const handle = lowerer.lowerExprExpecting(access.expression, F64); const fn = isImmediate ? "timers.immediateHasRef" : "timers.hasRef"; return { kind: "libCall", fn, args: [handle], type: BOOL, loc }; } - if (name === "refresh" && call.arguments.length === 0 && !isImmediate) { + if (name === "refresh" && call.arguments.nodes.length === 0 && !isImmediate) { // Re-arms to now + the original delay; yields the handle back for // chaining, like unref/ref. const handle = lowerer.lowerExprExpecting(access.expression, F64); @@ -237,11 +237,11 @@ export function lowerTimersPromisesCall( return lowerSchedulerCall(lowerer, expr, bi.member, loc); if (bi.member !== "setTimeout" && bi.member !== "setImmediate") return null; const timeout = bi.member === "setTimeout"; - const [valueNode, optionsNode] = timeout ? expr.arguments.slice(1) : expr.arguments; + const [valueNode, optionsNode] = timeout ? expr.arguments.nodes.slice(1) : expr.arguments.nodes; const ms: IrExpr | null = !timeout ? null - : expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], F64) + : expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], F64) : { kind: "numLit", value: 1, type: F64, loc }; if (valueNode === undefined) { return timeout @@ -311,7 +311,7 @@ function lowerSchedulerCall( // The scheduler.wait libCalls emit timers/promises setTimeout's runtime // calls; mock.timers mocks scheduler.wait and leaves setTimeout's named // and namespace imports real. An omitted delay is NaN, Node's undefined. - const [delayNode, optionsNode] = expr.arguments; + const [delayNode, optionsNode] = expr.arguments.nodes; const ms: IrExpr = delayNode ? lowerer.lowerExprExpecting(delayNode, F64) : { kind: "numLit", value: NaN, type: F64, loc }; @@ -355,7 +355,7 @@ function timersPromisesOptions( } let ref: IrExpr = absent; let signal: IrExpr | null = null; - for (const prop of optionsNode.properties) { + for (const prop of optionsNode.properties.nodes) { const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; if ( name === "signal" && diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index daf857650..3de973335 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -159,7 +159,7 @@ export function lowerUrlMethodCall( if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "url") return null; if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; - if ((name === "toString" || name === "toJSON") && call.arguments.length === 0) { + if ((name === "toString" || name === "toJSON") && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "url.href", args: [receiver], type: STRING, loc: locOf(call) }; } @@ -185,7 +185,7 @@ export function lowerSearchParamsNew( expr: ts.NewExpression, loc: SrcLoc, ): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `new URLSearchParams with ${args.length} arguments`, @@ -203,7 +203,7 @@ export function lowerSearchParamsNew( // type already rejects spreads' surprises, but keep the fence tight). if (ts.isObjectLiteralExpression(arg)) { let acc: IrExpr = { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc }; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop) || prop.name === undefined) { lowerer.unsupported( "SC1090", @@ -450,7 +450,7 @@ export function lowerSearchParamsMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; // The WHATWG arity ladder: too few arguments throw Node's // ERR_MISSING_ARGS before any conversion (the invalid-input probes' // `params.get()`). Claimed for effect-free receivers only — the @@ -661,7 +661,7 @@ function lowerSpForEachCall( access: ts.PropertyAccessExpression, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const fnArg = lowerer.lowerExpr(argNode); if ( fnArg.type.kind !== "func" || @@ -758,7 +758,7 @@ export function lowerFileUrlCall( loc: SrcLoc, hasSpread: boolean, ): IrExpr { - if (hasSpread || expr.arguments.length > 2) { + if (hasSpread || expr.arguments.nodes.length > 2) { lowerer.noLowering( `${bi.member} with these arguments`, expr, @@ -767,8 +767,8 @@ export function lowerFileUrlCall( } // Keep the established typed one-argument paths free of boxing. The // checked variants preserve each converter's validation/getter order. - if (expr.arguments.length === 1 && bi.member !== "fileURLToPathBuffer") { - const arg = lowerer.lowerExpr(expr.arguments[0]!); + if (expr.arguments.nodes.length === 1 && bi.member !== "fileURLToPathBuffer") { + const arg = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (bi.member === "fileURLToPath") { if (arg.type.kind === "url") return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [arg], type: STRING, loc }; @@ -792,8 +792,8 @@ export function lowerFileUrlCall( }; } const args = [0, 1].map((index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; @@ -818,7 +818,7 @@ export function lowerLegacyUrlCall( ): IrExpr { const arity = bi.member === "parse" ? 3 : bi.member === "format" || bi.member === "resolve" ? 2 : 1; - if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length > arity) { + if (expr.arguments.nodes.some(ts.isSpreadElement) || expr.arguments.nodes.length > arity) { lowerer.noLowering( `url.${bi.member} with these arguments`, expr, @@ -827,7 +827,7 @@ export function lowerLegacyUrlCall( } const args: IrExpr[] = []; for (let index = 0; index < arity; index++) { - let arg = expr.arguments[index]; + let arg = expr.arguments.nodes[index]; while (arg && ts.isParenthesizedExpression(arg)) arg = arg.expression; // An inline urlObject or options literal builds as the checked-dynamic // object format() reads (nested query objects included). @@ -840,7 +840,7 @@ export function lowerLegacyUrlCall( ); } if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") { - args.push({ kind: "boolLit", value: expr.arguments.length === 0, type: BOOL, loc }); + args.push({ kind: "boolLit", value: expr.arguments.nodes.length === 0, type: BOOL, loc }); } else { const warn = !insideNodeModules(expr.getSourceFile().fileName); args.push({ kind: "boolLit", value: warn, type: BOOL, loc }); @@ -855,7 +855,7 @@ export function isSearchParamsIterable(lowerer: Lowerer, node: ts.Expression): b if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "searchParams") return true; if ( !ts.isCallExpression(node) || - node.arguments.length !== 0 || + node.arguments.nodes.length !== 0 || !ts.isPropertyAccessExpression(node.expression) ) return false; diff --git a/packages/compiler/src/frontend/lowering/builtins/util-types.ts b/packages/compiler/src/frontend/lowering/builtins/util-types.ts index 4399c730e..2ca8e934c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/util-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/util-types.ts @@ -10,15 +10,15 @@ export function lowerUtilTypeCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with spread arguments`, expr); - const first = expr.arguments[0]; + const first = expr.arguments.nodes[0]; if (first !== undefined && lowerer.mapTypeOf(lowerer.typeOf(first))?.kind === "date") { // A Date value has no checked-dynamic form; its static type decides // every brand. The arguments still evaluate in order. return { kind: "seqExpr", - stmts: expr.arguments.map((arg): IrStmt => ({ + stmts: expr.arguments.nodes.map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc, @@ -28,15 +28,15 @@ export function lowerUtilTypeCall( loc, }; } - const input = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilTypeInput", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: saved.id, init: input, loc }, - ...expr.arguments + ...expr.arguments.nodes .slice(1) .map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc })), ], diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 2311e0f96..aa826cc53 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -40,7 +40,7 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): IrExpr | null { const constructor = expression.expression; if (!isWorkerConstructor(lowerer.program, constructor)) return null; - const args = expression.arguments ?? []; + const args = expression.arguments?.nodes ?? []; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "Worker constructor arguments", @@ -125,7 +125,7 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression): function capturesStdio(options: ts.Expression | undefined): boolean { if (!options) return false; if (!ts.isObjectLiteralExpression(options)) return true; - return options.properties.some((option) => { + return options.properties.nodes.some((option) => { if (ts.isSpreadAssignment(option)) return true; if (!option.name) return false; const name = propertyNameText(option.name); @@ -146,11 +146,11 @@ const HEAP_LIMITS = new Set([ * with numbers (Node ignores other values; the runtime refuses the rest). */ function refuseHeapLimits(lowerer: Lowerer, options: ts.Expression | undefined): void { if (!options || !ts.isObjectLiteralExpression(options)) return; - for (const option of options.properties) { + for (const option of options.properties.nodes) { if (!ts.isPropertyAssignment(option) || propertyNameText(option.name) !== "resourceLimits") continue; if (!ts.isObjectLiteralExpression(option.initializer)) continue; - for (const limit of option.initializer.properties) { + for (const limit of option.initializer.properties.nodes) { if (!ts.isPropertyAssignment(limit)) continue; const name = propertyNameText(limit.name); if (name === undefined || !HEAP_LIMITS.has(name)) continue; @@ -194,7 +194,7 @@ export function lowerMessageChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "MessageChannel")) return null; - if ((expression.arguments ?? []).length > 0) { + if ((expression.arguments?.nodes ?? []).length > 0) { lowerer.noLowering( "MessageChannel constructor arguments", expression, @@ -214,7 +214,7 @@ export function lowerMessageChannelNew( /** receiveMessageOnPort(port): { message } or undefined. */ export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`receiveMessageOnPort with ${args.length} arguments`, call); } @@ -244,7 +244,7 @@ export function lowerBroadcastChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "BroadcastChannel")) return null; - const args = expression.arguments ?? []; + const args = expression.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `BroadcastChannel constructor with ${args.length} arguments`, @@ -296,7 +296,7 @@ export function lowerEnvironmentData( call: ts.CallExpression, member: "setEnvironmentData" | "getEnvironmentData", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; const arity = member === "setEnvironmentData" ? 2 : 1; if (args.length > arity || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); @@ -331,7 +331,7 @@ export function lowerEnvironmentData( /** markAsUncloneable(value): structuredClone and postMessage then throw * DataCloneError for the value (scr_json.c). It needs no worker runtime. */ export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call); } @@ -354,7 +354,7 @@ export function lowerUntransferable( call: ts.CallExpression, member: "markAsUntransferable" | "isMarkedAsUntransferable", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); } diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 3119fa2ab..f5600b033 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -504,9 +504,9 @@ export function completeFuncValueArgs( mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required", })); shapes.push({ type: rest, mode: "rest" }); - return completeArgs(lowerer, call.arguments, shapes, loc, call); + return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call); } - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => lowerer.lowerExprExpecting(arg, funcType.params[index]), ); for (let i = args.length; i < funcType.params.length; i++) { @@ -553,7 +553,7 @@ export function lowerSpreadArgsCall( !(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined) ) return null; - if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (!expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) return null; if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null; const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) @@ -563,7 +563,7 @@ export function lowerSpreadArgsCall( : "value"; // Lower every argument ONCE, in source order (the IR nests them in // exactly this order, so runtime evaluation order is JS's). - const parts = expr.arguments.map((a) => + const parts = expr.arguments.nodes.map((a) => ts.isSpreadElement(a) ? { spreadOf: a.expression, node: null, v: lowerer.lowerExpr(a.expression) } : { spreadOf: null, node: a as ts.Expression, v: lowerer.lowerExpr(a) }, diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9375576f7..9d9c9668c 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -189,7 +189,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa executor && (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && + construction.arguments?.nodes[0] === executor && lowerer.isStdlibGlobal(construction.expression, "Promise"); if ( !nativeExecutor && @@ -634,10 +634,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // body (run()/discover() skip by the same test). Generic mixins still // register their generic signature below: non-mixin-shaped calls // degrade to the generic machinery's own per-site fences. - if (!decl.typeParameters && mixinFnShapeOf(lowerer, decl)) return; - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + if (!decl.typeParameters?.nodes && mixinFnShapeOf(lowerer, decl)) return; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = decl.asteriskToken !== undefined; - if (decl.typeParameters) { + if (decl.typeParameters?.nodes) { // Generic async composes: each monomorphized instance is an async // IrFunction like any other — its own spawn wrapper, its body // returning the resolved promise's inner (lowerGenericInstance). @@ -669,14 +669,14 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara } } - const params = lowerer.paramShapes(decl.parameters); + const params = lowerer.paramShapes(decl.parameters.nodes); // The zero-parameter declaration retains its dynRest ABI; value // functions use the full arguments slot even with no named parameters. appendArgumentsParameter( lowerer, decl, params, - decl.parameters.length > 0 ? "arguments" : "dynRest", + decl.parameters.nodes.length > 0 ? "arguments" : "dynRest", ); const nameBlame: ts.Node = decl.name ?? decl; const returnType = lowerer.declaredReturnType(decl, nameBlame); @@ -728,7 +728,7 @@ function appendArgumentsParameter( !bodyReadsArguments(node) ) return; - if (node.parameters.length > 0 || ts.isMethodDeclaration(node)) { + if (node.parameters.nodes.length > 0 || ts.isMethodDeclaration(node)) { const classMethod = ts.isMethodDeclaration(node) && (ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent)); @@ -765,7 +765,9 @@ export function lambdaSignature( isJsSourceFile(node.getSourceFile()) && !node.type && (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && - node.parameters.every((param) => !param.type && !param.dotDotDotToken && !param.initializer) && + node.parameters.nodes.every( + (param) => !param.type && !param.dotDotDotToken && !param.initializer, + ) && ts.isBinaryExpression(node.parent) && node.parent.right === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && @@ -775,11 +777,14 @@ export function lambdaSignature( ) { const receiver = lowerer.lowerExpr(node.parent.left.expression); if (receiver.type.kind === "dyn") { - const shapes: ParamShape[] = node.parameters.map(() => ({ type: DYN, mode: "required" })); + const shapes: ParamShape[] = node.parameters.nodes.map(() => ({ + type: DYN, + mode: "required", + })); return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } - if (node.typeParameters && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes && !lowerer.erasingLambdas.has(node)) { // Generic function-like forms monomorphize only where a static home // exists: top-level generic function declarations, generic methods // (class and object-literal), and module-scope never-reassigned @@ -797,14 +802,14 @@ export function lambdaSignature( : "generic arrow/function expressions outside a never-reassigned module-scope binding (only `const f = (x: T) => ...` bindings and top-level generic function declarations monomorphize)", ); } - const shapes = lowerer.paramShapes(node.parameters); + const shapes = lowerer.paramShapes(node.parameters.nodes); // Error-first callbacks captured by native filesystem adapters receive // the selected runtime overload. Ambient contextual inference can pick // the string overload even when options request a Buffer. if ( (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isCallExpression(node.parent) && - node.parent.arguments.at(-1) === node + node.parent.arguments.nodes.at(-1) === node ) { const builtin = ts.isPropertyAccessExpression(node.parent.expression) ? lowerer.builtinMemberOf(node.parent.expression) @@ -817,7 +822,7 @@ export function lambdaSignature( builtinModuleFnOf(lowerer, builtin.module, builtin.member)?.fn === "fs.callbackCall" ) { for (let index = 0; index < shapes.length; index++) { - if (!node.parameters[index]?.type) { + if (!node.parameters.nodes[index]?.type) { shapes[index]!.type = DYN; if (shapes[index]!.bodyType) shapes[index]!.bodyType = DYN; } @@ -854,7 +859,7 @@ export function lambdaSignature( // that recursive layout instead of first inferring narrower arrays. const isAsyncLike = !ts.isMethodDeclaration(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const innerRet = isAsyncLike && ret.kind === "promise" ? ret.inner : ret; // Union-inferred returns adopt too (a mixed-return body inferring a // SUB-union of the slot's union — adopting is a no-op when the two diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index d72792ae7..0fb5aadd3 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -287,12 +287,12 @@ export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStateme targets.push(e.left); e = e.right; } - if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false; + if (!ts.isObjectLiteralExpression(e) || e.properties.nodes.length !== 0) return false; if (targets.length !== 2) return false; const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t)); const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t)); if (!hasExports || !hasModuleExports) return false; - for (const prior of sf.statements) { + for (const prior of sf.statements.nodes) { if (prior === stmt) break; if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue; if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue; @@ -391,11 +391,11 @@ export function cjsExportCallIgnoresThis( // literal the statement names). let methodEntry: ts.MethodDeclaration | null = null; let shorthandOnly = false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind !== "table") continue; const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf); - for (const prop of table?.properties ?? []) { + for (const prop of table?.properties?.nodes ?? []) { if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name) continue; if (ts.isPropertyAssignment(prop)) values.push(prop.initializer); diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 7f31337f5..9b41f0012 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -75,7 +75,7 @@ export function cjsWholeExportValueOf( sf: ts.SourceFile, ): { assign: ts.BinaryExpression; rhs: ts.Expression } | null { let kept: ts.BinaryExpression | null = null; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr; } @@ -241,7 +241,7 @@ export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.Bindin const spec = requireSpecOf(decl.initializer); if (spec === null || canonicalBuiltinModule(spec) !== null) return []; if (!isJsSourceFile(decl.getSourceFile())) return []; - return decl.name.elements.filter( + return decl.name.elements.nodes.filter( (e) => e.name !== undefined && ts.isObjectBindingPattern(e.name) && @@ -324,7 +324,7 @@ export function cjsNamespaceDestructureOf( const dep = lowerer.cjsModuleBindingDepOf(init); if (dep === null) return null; const elements: { name: ts.Identifier; key: string }[] = []; - for (const e of decl.name.elements) { + for (const e of decl.name.elements.nodes) { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null; if (e.name === undefined || !ts.isIdentifier(e.name)) return null; const key = diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index f7955fc23..93a1e3260 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -47,7 +47,7 @@ export interface TscInteropBinding { * level: the tsc helper declaration shape. */ function tscHelperDeclName(stmt: ts.Statement): string | null { if (!ts.isVariableStatement(stmt)) return null; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) return null; const d = decls[0]!; if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined) @@ -74,7 +74,7 @@ function tscHelperDeclName(stmt: ts.Statement): string | null { /** True when the dependency stamps itself as an ES module translation. */ function stampsEsModule(dep: ts.SourceFile): boolean { - return dep.statements.some((s) => { + return dep.statements.nodes.some((s) => { if (!ts.isExpressionStatement(s)) return false; if (isEsModuleStamp(s.expression)) return true; const e = s.expression; @@ -106,12 +106,12 @@ export function tscInteropBindingOfDecl( if (!isCjsJsFile(sf, lowerer.program)) return null; let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; - if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null; + if (!ts.isCallExpression(init) || init.arguments.nodes.length !== 1) return null; const callee = init.expression; if (!ts.isIdentifier(callee)) return null; if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null; - if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null; - const spec = requireSpecOf(init.arguments[0]!); + if (!sf.statements.nodes.some((s) => tscHelperDeclName(s) === callee.text)) return null; + const spec = requireSpecOf(init.arguments.nodes[0]!); if (spec === null) return null; const dep = resolveImport(lowerer.program, sf, spec, "require") ?? @@ -177,7 +177,7 @@ export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boole if (name === null) return false; const sf = stmt.getSourceFile(); const helperSpans = new Map(); - for (const s of sf.statements) { + for (const s of sf.statements.nodes) { const n = tscHelperDeclName(s); if (n !== null) helperSpans.set(n, s); } diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index 36e280707..473ed2496 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -45,7 +45,7 @@ export function lowerClassDescriptorRead( ): IrExpr | null { if (!isDynTypedRefType(target.type)) return null; const info = lowerer.classes.get(target.type.className); - const key = call.arguments[1]!; + const key = call.arguments.nodes[1]!; if (!info || runtimeRooted(info) || !ts.isStringLiteral(key) || !bagOnlyProperty(info, key.text)) return null; const loc = locOf(call); @@ -79,7 +79,7 @@ export function lowerClassDataDescriptor( const safeName = (owner: ClassInfo, name: string): boolean => bagOnlyProperty(owner, name); const descriptor = (node: ts.Expression): boolean => ts.isObjectLiteralExpression(node) && - node.properties.every( + node.properties.nodes.every( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) || @@ -88,15 +88,15 @@ export function lowerClassDataDescriptor( literalName(p.name) ?? "", ), ); - const descriptors = call.arguments[member === "defineProperty" ? 2 : 1]!; + const descriptors = call.arguments.nodes[member === "defineProperty" ? 2 : 1]!; if (member === "defineProperty") { - const key = call.arguments[1]!; + const key = call.arguments.nodes[1]!; if ((ts.isStringLiteral(key) && !safeName(info, key.text)) || !descriptor(descriptors)) return null; } else { if ( !ts.isObjectLiteralExpression(descriptors) || - !descriptors.properties.every((p) => { + !descriptors.properties.nodes.every((p) => { if (!ts.isPropertyAssignment(p)) return false; const name = literalName(p.name); return ( @@ -127,13 +127,13 @@ export function lowerClassDataDescriptor( { kind: "varDecl" as const, localId: key.id, - init: lowerer.lowerExprExpecting(call.arguments[1]!, DYN), + init: lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), loc, }, ] : []), { kind: "varDecl", localId: desc.id, init: lowerer.lowerExprExpecting(descriptors, DYN), loc }, - ...(key && !ts.isStringLiteral(call.arguments[1]!) + ...(key && !ts.isStringLiteral(call.arguments.nodes[1]!) ? [ { kind: "assign" as const, diff --git a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts index 3b8e79398..a90ea90eb 100644 --- a/packages/compiler/src/frontend/lowering/class-instance-constructor.ts +++ b/packages/compiler/src/frontend/lowering/class-instance-constructor.ts @@ -43,7 +43,7 @@ export function lowerInstanceConstructorNew( const result = checkedClassConstruction( lowerer, constructor, - (expr.arguments ?? []).map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + (expr.arguments?.nodes ?? []).map((argument) => lowerer.lowerExprExpecting(argument, DYN)), loc, ); return { diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 75abeaf57..2a0e3a7eb 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -171,7 +171,7 @@ export function collectClassMethodMutations( const values = unwrap(loop.expression); if (!ts.isArrayLiteralExpression(values)) return null; const names: (string | number)[] = []; - for (const value of values.elements) { + for (const value of values.elements.nodes) { if (ts.isStringLiteralLike(value)) names.push(value.text); else if (ts.isNumericLiteral(value)) names.push(Number(value.text)); else return null; @@ -219,15 +219,15 @@ export function collectClassMethodMutations( const loop = statement.parent; if (!ts.isForOfStatement(loop) || loop.statement !== statement) return false; if (!ts.isVariableDeclarationList(loop.initializer)) return false; - const decl = loop.initializer.declarations[0]; + const decl = loop.initializer.declarations.nodes[0]; if (!decl || !ts.isIdentifier(decl.name) || !sameBinding(key, decl.name)) return false; const keys = unwrap(loop.expression); return ( ts.isCallExpression(keys) && ts.isPropertyAccessExpression(keys.expression) && lowerer.stdlibGlobalMember(keys.expression, "Object") === "keys" && - keys.arguments.length === 1 && - sameBinding(receiver, keys.arguments[0]!) + keys.arguments.nodes.length === 1 && + sameBinding(receiver, keys.arguments.nodes[0]!) ); }; const markKey = (key: ts.Expression, site: ts.Node, receiver?: ts.Expression): void => { @@ -244,7 +244,7 @@ export function collectClassMethodMutations( computedWrites.push({ key: undefined, receiver, site }); return; } - for (const field of value.properties) { + for (const field of value.properties.nodes) { if (ts.isSpreadAssignment(field)) markFields(field.expression, site, receiver); else if (field.name && (ts.isIdentifier(field.name) || ts.isStringLiteralLike(field.name))) markName(field.name.text, site); @@ -265,12 +265,12 @@ export function collectClassMethodMutations( } else if (ts.isElementAccessExpression(target)) markKey(target.argumentExpression, site, target.expression); else if (ts.isArrayLiteralExpression(target)) { - for (const element of target.elements) { + for (const element of target.elements.nodes) { if (ts.isSpreadElement(element)) markTarget(element.expression, site); else if (!ts.isOmittedExpression(element)) markTarget(element, site); } } else if (ts.isObjectLiteralExpression(target)) { - for (const field of target.properties) { + for (const field of target.properties.nodes) { if (ts.isPropertyAssignment(field)) markTarget(field.initializer, site); else if (ts.isSpreadAssignment(field)) markTarget(field.expression, site); } @@ -323,7 +323,7 @@ export function collectClassMethodMutations( if (!ts.isPropertyAssignment(node) || !ts.isArrowFunction(unwrap(node.initializer))) receiverFields.add(node.name.text); } - if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.modifiers?.length) + if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.modifiers?.nodes.length) receiverFields.add(node.name.text); if ( ts.isMethodDeclaration(node) && @@ -439,12 +439,12 @@ export function collectClassMethodMutations( unknownKey = node; continue; } - const receiver = call.arguments[0]; - if (single && call.arguments[1]) markKey(call.arguments[1], call, receiver); - else if (objectMethod === "defineProperties" && call.arguments[1]) - markFields(call.arguments[1], call, receiver); + const receiver = call.arguments.nodes[0]; + if (single && call.arguments.nodes[1]) markKey(call.arguments.nodes[1], call, receiver); + else if (objectMethod === "defineProperties" && call.arguments.nodes[1]) + markFields(call.arguments.nodes[1], call, receiver); else if (objectMethod === "assign") - for (const source of call.arguments.slice(1)) markFields(source, call, receiver); + for (const source of call.arguments.nodes.slice(1)) markFields(source, call, receiver); } lowerer.checker.prefetchClassCollection( computedWrites.flatMap((write) => [ @@ -468,7 +468,7 @@ export function collectClassMethodMutations( for (const cls of typedClasses) { const symbol = cls.name ? lowerer.checker.getSymbolAtLocation(cls.name) : undefined; if (!symbol) { - const declares = cls.members.some( + const declares = cls.members.nodes.some( (member) => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && @@ -482,7 +482,7 @@ export function collectClassMethodMutations( const property = lowerer.checker.getPropertyOfType(type, name); if (!property || !lowerer.checker.declarationsOf(property).some(ts.isMethodDeclaration)) continue; - if (cls.typeParameters !== undefined && cls.typeParameters.length > 0) { + if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) { owners = null; break; } @@ -517,7 +517,9 @@ export function collectClassMethodMutations( ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ) { - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + if ( + member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) return null; } else return null; const symbol = lowerer.checker.getSymbolAtLocation(cls.name); diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 50278c3b7..10572377e 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -348,7 +348,7 @@ function reflectedMethodValue( if (existing) return existing; const declaration = info.decl; if (!declaration) return null; - const member = declaration.members.find((member) => { + const member = declaration.members.nodes.find((member) => { if ( (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.name || diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 2f66a5203..c8e56f3cc 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -205,14 +205,15 @@ function protocolSignature( !signature || !declaration || !(ts.isMethodSignatureDeclaration(declaration) || ts.isMethodDeclaration(declaration)) || - (declaration.typeParameters?.length ?? 0) > 0 + (declaration.typeParameters?.nodes.length ?? 0) > 0 ) return null; - const declared = (declaration.parameters ?? []).filter( + const declared = (declaration.parameters.nodes ?? []).filter( (param) => !(ts.isIdentifier(param.name) && param.name.text === "this"), ); const symbols = signature.getParameters(); - if (symbols.length !== declared.length || call.arguments.length !== declared.length) return null; + if (symbols.length !== declared.length || call.arguments.nodes.length !== declared.length) + return null; const records = (id: string) => lowerer.shapes.get(id); const unions = (id: string) => lowerer.unions.get(id); const params: IrType[] = []; @@ -243,7 +244,7 @@ export function lowerProtocolMethodCall( recv.type.kind !== "dyn" || call.questionDotToken || access.questionDotToken || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; let signature: ProtocolSignature | null; @@ -266,11 +267,11 @@ export function lowerProtocolMethodCall( const stmts: IrStmt[] = receiverLocal ? [{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc }] : []; - const args = call.arguments.map((arg, i) => + const args = call.arguments.nodes.map((arg, i) => lowerer.coerceToExpected(lowerer.lowerExprExpecting(arg, params[i]!), params[i]!), ); const probed = - !args.every(effectFree) || !call.arguments.every((arg) => stableArgument(lowerer, arg)); + !args.every(effectFree) || !call.arguments.nodes.every((arg) => stableArgument(lowerer, arg)); const key = JSON.stringify([method, params.map(typeKey), typeKey(result), returnsVoid, probed]); let dispatch = lowerer.protocolDispatches.get(key); if (!dispatch) { diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 3c610c51a..9e69d08b7 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -29,9 +29,10 @@ export class ClassProtocols { node.kind === ts.SyntaxKind.ThisKeyword || ts.forEachChild(node, usesReceiver) === true; const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { - for (const clause of node.heritageClauses ?? []) + for (const clause of node.heritageClauses?.nodes ?? []) if (clause.token === ts.SyntaxKind.ImplementsKeyword) - for (const protocol of clause.types) add(this.checker.getTypeAtLocation(protocol)); + for (const protocol of clause.types.nodes) + add(this.checker.getTypeAtLocation(protocol)); const symbol = node.name ? this.checker.getSymbolAtLocation(node.name) : undefined; const instance = symbol ? this.checker.getDeclaredTypeOfSymbol(symbol) : undefined; for (const property of instance ? this.checker.getPropertiesOfType(instance) : []) { @@ -41,7 +42,7 @@ export class ClassProtocols { .some( (member) => ts.isMethodDeclaration(member) && - !member.modifiers?.some( + !member.modifiers?.nodes.some( (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, ), ) @@ -54,7 +55,7 @@ export class ClassProtocols { } if ( ts.isObjectLiteralExpression(node) && - node.properties.some( + node.properties.nodes.some( (property) => (ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || @@ -167,14 +168,14 @@ export class ClassProtocols { observed.push(node.expression); } else if (ts.isSwitchStatement(node)) { // `switch` matches its cases with ===. - const cases = node.caseBlock.clauses + const cases = node.caseBlock.clauses.nodes .filter(ts.isCaseClause) .map((clause) => clause.expression) .filter((expr) => !isNullishLiteral(expr) && !isPrimitiveLiteral(expr)); if (cases.length > 0) observed.push(node.expression, ...cases); } else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { const callee = node.expression; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; const member = ts.isPropertyAccessExpression(callee) ? callee.name.text : null; const owner = ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) @@ -204,7 +205,8 @@ export class ClassProtocols { }; const visit = (node: ts.Node): void => { observe(node); - if (ts.isCallExpression(node) || ts.isNewExpression(node)) node.arguments?.forEach(flow); + if (ts.isCallExpression(node) || ts.isNewExpression(node)) + node.arguments?.nodes.forEach(flow); else if (ts.isVariableDeclaration(node) && node.type !== undefined) flow(node.initializer); else if (ts.isPropertyDeclaration(node) && node.type !== undefined) flow(node.initializer); else if (ts.isParameter(node) && node.type !== undefined) flow(node.initializer); @@ -212,7 +214,7 @@ export class ClassProtocols { else if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) flow(node.body); else if (ts.isYieldExpression(node)) flow(node.expression); else if (ts.isArrayLiteralExpression(node)) - node.elements.forEach((element) => { + node.elements.nodes.forEach((element) => { if (!ts.isSpreadElement(element) && !ts.isOmittedExpression(element)) flow(element); }); else if (ts.isPropertyAssignment(node)) flow(node.initializer); @@ -234,7 +236,7 @@ export class ClassProtocols { }); } if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) - node.parameters.forEach((param, index) => { + node.parameters.nodes.forEach((param, index) => { if (param.type !== undefined) callbackParams.push({ fn: node, index, name: param.name }); }); ts.forEachChild(node, visit); diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 1db709e91..403a4a3b4 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -360,7 +360,7 @@ export function reflectClassPrototype( const context = newFnCtx(false, null, null, DYN); lowerer.fnStack.push(context); try { - for (const member of info.decl.members) { + for (const member of info.decl.members.nodes) { if ( (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body || @@ -430,12 +430,12 @@ export function lowerClassPrototypeData( const parent = expr.parent; const descriptorKey = ts.isCallExpression(parent) && - parent.arguments[0] === expr && + parent.arguments.nodes[0] === expr && ts.isPropertyAccessExpression(parent.expression) && lowerer.stdlibGlobalMember(parent.expression, "Object") === "defineProperty" && - parent.arguments[1] && - ts.isStringLiteral(parent.arguments[1]) - ? parent.arguments[1].text + parent.arguments.nodes[1] && + ts.isStringLiteral(parent.arguments.nodes[1]) + ? parent.arguments.nodes[1].text : null; const name = ts.isPropertyAccessExpression(parent) && parent.expression === expr @@ -481,12 +481,12 @@ export function lowerClassPrototypeAssign( lowerer: Lowerer, call: ts.CallExpression, ): IrExpr | null { - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; if (!target || !ts.isPropertyAccessExpression(target) || target.name.text !== "prototype") return null; const info = exactClassOfReceiver(lowerer, target.expression); if (!info) return null; - if (!ts.isExpressionStatement(call.parent) || call.arguments.some(ts.isSpreadElement)) { + if (!ts.isExpressionStatement(call.parent) || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "class prototype assignment results and spread arguments"); } const loc = locOf(call); @@ -507,7 +507,9 @@ export function lowerClassPrototypeAssign( target, "prototype data on generic, mixin, or runtime-provided classes", ); - const sources = call.arguments.slice(1).map((node) => lowerer.lowerExprExpecting(node, DYN)); + const sources = call.arguments.nodes + .slice(1) + .map((node) => lowerer.lowerExprExpecting(node, DYN)); return { kind: "libCall", fn: "dyn.assignPrototype", @@ -685,11 +687,11 @@ export function lowerClassPrototypeDescriptors( call: ts.CallExpression, member: string, ): IrExpr | null { - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; if ( member !== "defineProperties" || - call.arguments.length !== 2 || - call.arguments.some(ts.isSpreadElement) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some(ts.isSpreadElement) || !target || !ts.isPropertyAccessExpression(target) || target.name.text !== "prototype" @@ -720,7 +722,7 @@ export function lowerClassPrototypeDescriptors( fn: "dyn.definePrototypeProps", args: [ prototype, - lowerer.lowerExprExpecting(call.arguments[1]!, DYN), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), prototypeProtectedKeys(lowerer, info, target, loc), ], type: DYN, diff --git a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts index cc658f872..310c8f389 100644 --- a/packages/compiler/src/frontend/lowering/class-runtime-statics.ts +++ b/packages/compiler/src/frontend/lowering/class-runtime-statics.ts @@ -65,7 +65,7 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu ); for (const member of [...definitions, ...initializers]) { if (ts.isClassStaticBlockDeclaration(member)) { - statements.push(...lowerer.lowerStmts(member.body.statements)); + statements.push(...lowerer.lowerStmts(member.body.statements.nodes)); continue; } if (!member.name) continue; diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts index 0e8f64a7b..af5425727 100644 --- a/packages/compiler/src/frontend/lowering/class-static-data.ts +++ b/packages/compiler/src/frontend/lowering/class-static-data.ts @@ -46,7 +46,7 @@ export function classStaticDataFor( if (findStaticOn(lowerer, info, name) || findGenericStaticOn(lowerer, info, name)) return null; for (let c: ClassInfo | null = info; c; c = c.base) { if ( - c.decl?.members.some( + c.decl?.members?.nodes.some( (m) => m.name && ts.canHaveModifiers(m) && diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 1ff401b4e..78c7cb73d 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -407,7 +407,7 @@ export function recordToClassPlan( return null; } } - if (!info.ctor || info.ctor.body === undefined || info.ctor.body.statements.length > 0) + if (!info.ctor || info.ctor.body === undefined || info.ctor.body.statements.nodes.length > 0) return null; const props = info.paramProps ?? []; if (props.length !== info.ctorParams.length) return null; diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 9fc1902c7..5449b0943 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -53,7 +53,7 @@ export function collectionInput( if ( ts.isCallExpression(expression) && !expression.questionDotToken && - expression.arguments.length === 0 && + expression.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(expression.expression) && !expression.expression.questionDotToken ) { diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts index e2bd82516..77f72dc15 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts @@ -13,8 +13,8 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo }); if (!decl) throw new Error("fixture has no class"); const cls = decl; - const ctor = cls.members.find(ts.isConstructorDeclaration) ?? null; - const fields = cls.members + const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null; + const fields = cls.members.nodes .filter(ts.isPropertyDeclaration) .map((member) => ({ name: member.name.getText(), initializer: member.initializer })); const fieldNames = new Set(fields.map((field) => field.name)); @@ -28,7 +28,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo privateMethodBody: (call) => { if (!ts.isPropertyAccessExpression(call.expression)) return null; const name = call.expression.name.text; - const method = cls.members.find( + const method = cls.members.nodes.find( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && member.name.getText() === name, ); diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index 484920a1c..b9c8d7e07 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -89,10 +89,11 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) return true; if (ts.isCallExpression(node) && thisMember(node.expression)) { - if (node.arguments.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) return true; + if (node.arguments.nodes.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) + return true; const body = depth < HELPER_DEPTH ? inputs.privateMethodBody(node) : null; if (!body) return true; - return !followStatements(body.statements, depth + 1); + return !followStatements(body.statements.nodes, depth + 1); } if (thisMember(node)) { const name = node.name.text; @@ -137,7 +138,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca } return true; }; - const statements = inputs.ctor?.body?.statements ?? []; + const statements = inputs.ctor?.body?.statements?.nodes ?? []; let index = 0; if (inputs.derived) { // Own initializers run when super() returns. A base construction that @@ -149,7 +150,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca ts.isCallExpression(stmt.expression) && stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword; if (isSuper) { - if (stmt.expression.arguments.some((arg) => mentionsReceiver(arg))) return stop(); + if (stmt.expression.arguments.nodes.some((arg) => mentionsReceiver(arg))) return stop(); break; } // super() nested inside other statements: keep the analysis simple. diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts index b88de3795..a22a82bb1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -16,7 +16,7 @@ export function lowerArrayCallbackCall( access: ts.ElementAccessExpression, array: IrExpr, ): IrExpr | null { - if (array.type.kind !== "array" || call.arguments.some(ts.isSpreadElement)) return null; + if (array.type.kind !== "array" || call.arguments.nodes.some(ts.isSpreadElement)) return null; const elem = array.type.elem; const arms = elem.kind === "union" ? (lowerer.unions.get(elem.unionId)?.arms ?? []) : [elem]; const functions = arms.filter((arm) => arm.kind === "func"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index a79864983..2e93e2fd5 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -74,7 +74,7 @@ export function lowerArrayHofCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const booleanFilter = method === "filter" && @@ -341,7 +341,7 @@ function lowerFastCallback( // The optional-read prepass binds an unannotated element parameter to // the array's runtime value type. Rebind it to the plain element for // this lowering only. - const param = argNode.parameters[elemIndex]; + const param = argNode.parameters.nodes[elemIndex]; if (!param || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name)) return null; const symbol = param.type ? undefined : lowerer.checker.getSymbolAtLocation(param.name); @@ -563,7 +563,7 @@ export function lowerArrayUnionHofCall( if (!lowerer.isStdlibMember(access)) return null; const def = lowerer.unions.get(receiverT.unionId); if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "array")) return null; - if (call.arguments.length !== 1 || !call.arguments[0]) return null; + if (call.arguments.nodes.length !== 1 || !call.arguments.nodes[0]) return null; const arrays = def.arms as (IrType & { kind: "array" })[]; const valueArms: IrType[] = []; const addValueArm = (type: IrType): boolean => { @@ -586,7 +586,7 @@ export function lowerArrayUnionHofCall( valueArms.length === 1 ? valueArms[0]! : { kind: "union", unionId: lowerer.unions.intern(valueArms) }; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [valueT], receiverT); const fnRet = fnArg.type.ret; if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) { @@ -1025,7 +1025,7 @@ export function lowerArrayFindLikeCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded // find's result must be reachable from the element itself; a predicate // that narrows to another layout keeps the optional element visit. @@ -1495,7 +1495,7 @@ export function lowerArrayFlatMapCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const present = presentElements(lowerer, call, elem); const { fnArg, arity } = lowerArrayCallback( @@ -1779,9 +1779,9 @@ export function lowerArrayReduceCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded - const hasInit = call.arguments.length === 2; + const hasInit = call.arguments.nodes.length === 2; const accT = lowerer.runtimeOptionalReduceTypes.get(call) ?? (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem)); @@ -1818,7 +1818,7 @@ export function lowerArrayReduceCall( fast?.type ?? null, ); const args: IrExpr[] = [receiver, fnArg]; - if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, accT)); + if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, accT)); // The fast closure is created last; making a closure has no observable // effect, so the initial value still evaluates right after the callback. if (fast) args.push(fast); @@ -2037,7 +2037,7 @@ export function lowerArraySortCall( const method = access.name.text === "toSorted" ? "toSorted" : "sort"; const copyFirst = method === "toSorted"; if ( - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") ) { return { @@ -2049,15 +2049,15 @@ export function lowerArraySortCall( loc, }; } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "default ordering supports string[], number[] and boolean[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, argNode); let unwrappedArg = argNode; while ( @@ -2205,8 +2205,8 @@ export function lowerDynArrayFilterCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); // The result element type, from what the call flows INTO: the // contextual array (or the single array arm of a contextual union — @@ -2270,8 +2270,8 @@ export function lowerDynArrayFlatMapCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [DYN], DYN); const ret = fnArg.type.ret; @@ -2631,7 +2631,7 @@ export function lowerTuplePredicateCall( reads: IrExpr[], ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const booleanFilter = method === "filter" && ts.isIdentifier(argNode) && diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 57b8caf14..4f6e4ca43 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -144,7 +144,7 @@ export function arrayConcatHelper( }, ...append(varRef("a.0", arrT, loc), elem, node.expression), ...shape.flatMap((type, i) => - append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments[i]!), + append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments.nodes[i]!), ), { kind: "return", value: out, loc }, ]; diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 015cb2123..6072b58b7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -448,12 +448,12 @@ export function lowerArrayOfCall( if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { lowerer.badType(call, lowerer.typeOf(call)); } - if (call.arguments.some(ts.isSpreadElement)) { - return lowerArraySpreadItems(lowerer, call.arguments, result.elem, result, locOf(call)); + if (call.arguments.nodes.some(ts.isSpreadElement)) { + return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call)); } return { kind: "arrayLit", - elems: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), + elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), type: result, loc: locOf(call), }; @@ -795,7 +795,7 @@ export function lowerArrayFromCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "from") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if ( isJsSourceFile(call.getSourceFile()) && args.length >= 1 && @@ -884,9 +884,9 @@ export function lowerArrayFromCall( if ( args.length === 1 && ts.isObjectLiteralExpression(args[0]!) && - args[0]!.properties.length === 1 + args[0]!.properties.nodes.length === 1 ) { - const n = lowerLengthProp(lowerer, args[0]!.properties[0]!); + const n = lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!); if (n) { if (n.type.kind !== "f64") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -941,7 +941,7 @@ export function lowerArrayFromCall( const valueT = arrayValueType(lowerer, arrT.elem); const firstParam = ts.isArrowFunction(mapper) || ts.isFunctionExpression(mapper) - ? mapper.parameters[0]?.name + ? mapper.parameters.nodes[0]?.name : undefined; const paramSymbol = firstParam && ts.isIdentifier(firstParam) @@ -1021,8 +1021,10 @@ export function lowerArrayFromCall( ); } const n = - args.length === 2 && ts.isObjectLiteralExpression(args[0]!) && args[0]!.properties.length === 1 - ? lowerLengthProp(lowerer, args[0]!.properties[0]!) + args.length === 2 && + ts.isObjectLiteralExpression(args[0]!) && + args[0]!.properties.nodes.length === 1 + ? lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!) : null; if (!n) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts index db60cdf1b..7aeec62ed 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -43,7 +43,7 @@ export function lowerArrayFromAsyncCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "fromAsync") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `Array.fromAsync with ${args.length} arguments`, diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 40c2e730d..7ef96457c 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -318,7 +318,8 @@ export function lowerArrayMethodCall( if (lowerer.chainBlocked(access, call)) return null; const name = access.name.text; const iteratorMethod = - (name === "values" || name === "keys" || name === "entries") && call.arguments.length === 0; + (name === "values" || name === "keys" || name === "entries") && + call.arguments.nodes.length === 0; if ( !ARRAY_METHODS.has(name) && name !== "sort" && @@ -446,8 +447,8 @@ export function lowerArrayMethodCall( return lowerArraySortCall(lowerer, call, access, elem, receiverIr); } if (name === "toReversed") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.toReversed with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.toReversed with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -459,8 +460,8 @@ export function lowerArrayMethodCall( }; } if (name === "reverse") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.reverse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.reverse with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -472,10 +473,10 @@ export function lowerArrayMethodCall( }; } if (name === "fill") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.fill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.fill with ${call.arguments.nodes.length} arguments`, call); } - const value = call.arguments[0] ? lowerer.lowerExpr(call.arguments[0]) : null; + const value = call.arguments.nodes[0] ? lowerer.lowerExpr(call.arguments.nodes[0]) : null; if ( value && value.type.kind === "dyn" && @@ -485,7 +486,7 @@ export function lowerArrayMethodCall( ) { lowerer.noLowering( ".fill of a checked-dynamic reference into a static array", - call.arguments[0]!, + call.arguments.nodes[0]!, ); } const writeUndefined = @@ -495,35 +496,53 @@ export function lowerArrayMethodCall( lowerer.lowerExpr(access.expression), value, writeUndefined, - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array fill start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array fill end"), + lowerArrayPosition(lowerer, call.arguments.nodes[1], numLit(0, loc), "array fill start"), + lowerArrayPosition(lowerer, call.arguments.nodes[2], numLit(Infinity, loc), "array fill end"), receiverIr, loc, ); } if (name === "copyWithin") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.copyWithin with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.copyWithin with ${call.arguments.nodes.length} arguments`, call); } return lowerArrayCopyWithin( lowerer.lowerExpr(access.expression), - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array copyWithin target"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array copyWithin start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array copyWithin end"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[0], + numLit(0, loc), + "array copyWithin target", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(0, loc), + "array copyWithin start", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[2], + numLit(Infinity, loc), + "array copyWithin end", + ), receiverIr, loc, ); } if (name === "with") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.with with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.with with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const index = lowerPositionArgument(lowerer, call.arguments[0]!, numLit(0, loc)); + const index = lowerPositionArgument(lowerer, call.arguments.nodes[0]!, numLit(0, loc)); if (!primitivePositionType(lowerer, index.type)) { - lowerer.noLowering(`.with index of '${lowerer.fmt(index.type)}' values`, call.arguments[0]!); + lowerer.noLowering( + `.with index of '${lowerer.fmt(index.type)}' values`, + call.arguments.nodes[0]!, + ); } - const value = lowerer.lowerExpr(call.arguments[1]!); + const value = lowerer.lowerExpr(call.arguments.nodes[1]!); const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr); const indexLocal = lowerer.declareHiddenLocal("%withIndex", index.type); const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type); @@ -534,7 +553,7 @@ export function lowerArrayMethodCall( lowerer, indexRef, numLit(0, loc), - call.arguments[0]!, + call.arguments.nodes[0]!, "array with index", ); const stmts: IrStmt[] = [ @@ -588,7 +607,7 @@ export function lowerArrayMethodCall( loc, }; } - const coercedValue = lowerer.coerceInto(call.arguments[1]!, valueRef, elem); + const coercedValue = lowerer.coerceInto(call.arguments.nodes[1]!, valueRef, elem); return { kind: "seqExpr", stmts, @@ -608,17 +627,17 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const start = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(0, loc), "array toSpliced start", ); const deleteCount = lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array toSpliced deleteCount", ); - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -709,7 +728,7 @@ export function lowerArrayMethodCall( | "reduce" | "reduceRight" ]; - if (call.arguments.length < arity[0]! || call.arguments.length > arity[1]!) { + if (call.arguments.nodes.length < arity[0]! || call.arguments.nodes.length > arity[1]!) { const hint = name === "map" || name === "filter" || @@ -721,7 +740,7 @@ export function lowerArrayMethodCall( ? "the thisArg parameter has no lowering — use an arrow function" : undefined; lowerer.noLowering( - `.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, + `.${name} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, call, hint, ); @@ -742,8 +761,8 @@ export function lowerArrayMethodCall( // order (count snapshotted first, so the self-spread forms duplicate // like JS) and return the new length. const spreadArg = - call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!) - ? call.arguments[0]! + call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!) + ? call.arguments.nodes[0]! : null; if ((name === "push" || name === "unshift") && spreadArg && ts.isSpreadElement(spreadArg)) { let src = lowerer.lowerExpr(spreadArg.expression); @@ -790,11 +809,11 @@ export function lowerArrayMethodCall( loc, }; } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { // Finish argument evaluation before mutating the receiver. Each // spread snapshots its elements immediately, so a later argument // can mutate that source without changing already-collected values. - const items = lowerArraySpreadItems(lowerer, call.arguments, elem, receiverIr, loc); + const items = lowerArraySpreadItems(lowerer, call.arguments.nodes, elem, receiverIr, loc); return { kind: "arrIntrinsic", method: name === "push" ? "pushSpread" : "unshiftSpread", @@ -804,7 +823,7 @@ export function lowerArrayMethodCall( loc, }; } - const valueProbes = call.arguments.map((arg) => tryLowerExpression(lowerer, arg)); + const valueProbes = call.arguments.nodes.map((arg) => tryLowerExpression(lowerer, arg)); if ( valueProbes.some( (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, @@ -814,14 +833,14 @@ export function lowerArrayMethodCall( return lowerOptionalArrayPush( lowerer, receiver, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, loc, ); } const items = lowerArrayValueItems( lowerer, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc, @@ -838,7 +857,7 @@ export function lowerArrayMethodCall( // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). - const args = call.arguments.map((a) => + const args = call.arguments.nodes.map((a) => name === "push" || name === "unshift" ? lowerer.lowerExprExpecting(a, elem) : lowerer.lowerExpr(a), @@ -886,7 +905,7 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const shape: (IrType | null)[] = []; const args: IrExpr[] = []; - for (const argNode of call.arguments) { + for (const argNode of call.arguments.nodes) { if (ts.isSpreadElement(argNode)) { lowerer.unsupported( "SC1090", @@ -928,26 +947,31 @@ export function lowerArrayMethodCall( if (name === "slice") { const receiver = lowerer.lowerExpr(access.expression); const args = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array slice start"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(Infinity, loc), "array slice end"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array slice start"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(Infinity, loc), + "array slice end", + ), ]; return { kind: "arrIntrinsic", method: "slice", receiver, args, type: receiverIr, loc }; } if (name === "splice") { const receiver = lowerer.lowerExpr(access.expression); const args: IrExpr[] = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array splice start"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array splice start"), lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array splice deleteCount", ), ]; - if (call.arguments.length <= 2) { + if (call.arguments.nodes.length <= 2) { return { kind: "arrIntrinsic", method: "splice", receiver, args, type: receiverIr, loc }; } - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -1007,8 +1031,8 @@ export function lowerArrayMethodCall( 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)] + const args = call.arguments.nodes.length + ? [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)] : []; return lowerer.coerceToExpected( { @@ -1047,7 +1071,7 @@ export function lowerArrayMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const sep = lowerArrayJoinSeparator(lowerer, call.arguments[0], loc); + const sep = lowerArrayJoinSeparator(lowerer, call.arguments.nodes[0], loc); return { kind: "arrIntrinsic", method: "join", receiver, args: [sep], type: STRING, loc }; } if (name === "map" || name === "filter" || name === "forEach") { @@ -1247,8 +1271,8 @@ function lowerArrayFlatCall( receiverType: IrType & { kind: "array" }, ): IrExpr { const loc = locOf(call); - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.flat with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.flat with ${call.arguments.nodes.length} arguments`, call); } const elemCouldNest = (elem: IrType): boolean => elem.kind === "array" || @@ -1258,11 +1282,11 @@ function lowerArrayFlatCall( .get(elem.unionId) ?.arms.some((arm) => arm.kind === "array" || arm.kind === "dyn") ?? true)); - if (!elemCouldNest(receiverType.elem) && call.arguments[0]) { + if (!elemCouldNest(receiverType.elem) && call.arguments.nodes[0]) { const receiver = lowerer.lowerExpr(access.expression); const depth = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(1, loc), "array flat depth", ); @@ -1286,7 +1310,9 @@ function lowerArrayFlatCall( loc, }; } - const depth = call.arguments[0] ? literalFlatDepth(lowerer.lowerExpr(call.arguments[0])) : 1; + const depth = call.arguments.nodes[0] + ? literalFlatDepth(lowerer.lowerExpr(call.arguments.nodes[0])) + : 1; if (depth === null) lowerer.noLowering(".flat with a nonconstant depth", call); let remaining = Number.isNaN(depth) || depth <= 0 ? 0 : Math.trunc(depth); let result: IrExpr = lowerer.lowerExpr(access.expression); @@ -1338,13 +1364,13 @@ function lowerArraySearchCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`.${method} with spread arguments`, call); - const needle: IrExpr = call.arguments[0] - ? lowerer.lowerExpr(call.arguments[0]) + const needle: IrExpr = call.arguments.nodes[0] + ? lowerer.lowerExpr(call.arguments.nodes[0]) : { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; - const fromIndex: IrExpr = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const fromIndex: IrExpr = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : method === "lastIndexOf" ? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc } : numLit(0, loc); @@ -1361,7 +1387,7 @@ function lowerArraySearchValues( fromIndex: IrExpr, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0] ?? call; + const argNode = call.arguments.nodes[0] ?? call; // A derived CLASS VALUE against a base-classval element widens (the same // pointer — identity search is exact); the coercion path owns the ABI gate. if ( @@ -1376,7 +1402,7 @@ function lowerArraySearchValues( // runtime implementation already has exact primitive/reference equality. if ( method !== "lastIndexOf" && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem) @@ -1398,7 +1424,7 @@ function lowerArraySearchValues( if (valueT.kind !== "union") { if (method === "lastIndexOf") lowerer.noLowering(".lastIndexOf on an array of checked values", call); - if (call.arguments.length > 1) + if (call.arguments.nodes.length > 1) lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call); if (!typeEquals(needle.type, elem)) lowerer.badType(argNode, lowerer.typeOf(argNode)); return { @@ -1456,7 +1482,7 @@ function lowerArraySearchValues( searchNeedle.type, directNeedle, fromIndex.type, - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, loc, ); return { @@ -1919,10 +1945,10 @@ export function lowerTupleReadMethodCall( elem, ); if (search) { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { return lowerer.noLowering(`tuple .${method} with spread or extra arguments`, call); } - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; const undefinedArg = arg ? lowerStaticallyUndefinedArgument(lowerer, arg) : null; let needle = arg ? (undefinedArg ?? lowerer.lowerExpr(arg)) : null; if (!needle || isUnitType(needle.type) || needle.type.kind === "void") { @@ -1934,10 +1960,10 @@ export function lowerTupleReadMethodCall( if (!wrapped) return lowerer.noLowering(`tuple .${method} missing search value`, call); needle = needle ? defaultAfterUndefined(needle, wrapped) : wrapped; } - const from = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const from = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : numLit(method === "lastIndexOf" ? Infinity : 0, loc); - const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.length}`; + const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.nodes.length}`; let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { @@ -1978,7 +2004,7 @@ export function lowerTupleReadMethodCall( lowerer, varRef("from.0", from.type, loc), numLit(0, loc), - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "tuple search position", ), loc, @@ -1991,13 +2017,13 @@ export function lowerTupleReadMethodCall( return { kind: "call", callee: helper, args: [receiver, needle, from], type: resultType, loc }; } if (method === "slice") { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.slice with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.slice with ${call.arguments.nodes.length} arguments`, call); } - const args = call.arguments.map((arg) => lowerer.lowerExpr(arg)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)); args.forEach((arg, i) => { if (arg.type.kind !== "f64") - lowerer.badType(call.arguments[i]!, lowerer.typeOf(call.arguments[i]!)); + lowerer.badType(call.arguments.nodes[i]!, lowerer.typeOf(call.arguments.nodes[i]!)); }); const resultType = arrayOf(elem); const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; @@ -2039,14 +2065,14 @@ export function lowerTupleReadMethodCall( } return { kind: "call", callee: helper, args: [receiver, ...args], type: resultType, loc }; } - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); } - const callback = call.arguments[0]!; + const callback = call.arguments.nodes[0]!; // Check before the array HOF adapter can create a tuple-to-array copy. const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(callback)); if (signatures.some((signature) => signature.getParameters().length > 2)) { @@ -2210,10 +2236,10 @@ function lowerArrayAtCall( elem: IrType, ): IrExpr { const loc = locOf(call); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(".at with spread arguments", call); const receiver = lowerer.lowerExpr(access.expression); - const indexNode = call.arguments[0]; + const indexNode = call.arguments.nodes[0]; const index = lowerPositionArgument(lowerer, indexNode, numLit(0, loc)); const resultT = lowerer.irTypeOf(call); if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 91cf0b263..7f1f758ca 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -50,15 +50,15 @@ function lowerBytesSortCall( const loc = locOf(call); const method = access.name.text; const copyFirst = method === "toSorted"; - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, + `.${method} with ${call.arguments.nodes.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, call, ); } const receiver = lowerer.lowerExprExpecting(access.expression, bytesT); - const undefinedArg = call.arguments[0] - ? lowerStaticallyUndefinedArgument(lowerer, call.arguments[0]) + const undefinedArg = call.arguments.nodes[0] + ? lowerStaticallyUndefinedArgument(lowerer, call.arguments.nodes[0]) : null; const helperFor = ( key: string, @@ -77,7 +77,7 @@ function lowerBytesSortCall( } return helper; }; - if (call.arguments.length === 0 || undefinedArg) { + if (call.arguments.nodes.length === 0 || undefinedArg) { const helper = helperFor("default", 0, false, null); if (!undefinedArg || isSafeToDiscard(undefinedArg)) { return { kind: "call", callee: helper, args: [receiver], type: bytesT, loc }; @@ -109,7 +109,7 @@ function lowerBytesSortCall( loc, }; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let fnArg = lowerer.lowerExpr(argNode); // As for Array sort: adapt a comparator over checked values to the // element ABI; arithmetic on inferred JS values still yields a number. @@ -189,7 +189,7 @@ export function lowerBytesNew( (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") && lowerer.isStdlibSymbol(symbol) ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "resizable ArrayBuffer construction", @@ -216,15 +216,15 @@ export function lowerBytesNew( const name = symbol.name; const type = bytesOf(elem); const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length === 0) return { kind: "bytesNew", source: null, type, loc }; if (args.length >= 2 && args.length <= 3 && !args.some(ts.isSpreadElement)) { return lowerArrayBufferView(lowerer, args, elem, loc); } if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { const argNode = args[0]!; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); + if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.nodes.some(ts.isSpreadElement)) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "bytesNew", source: seed, type, loc }; } @@ -277,9 +277,9 @@ export function lowerBytesStaticCall( ): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; if (lowerer.isStdlibGlobal(access.expression, "ArrayBuffer") && access.name.text === "isView") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("ArrayBuffer.isView argument count", call); - const arg = call.arguments[0]!; + const arg = call.arguments.nodes[0]!; return { kind: "libCall", fn: "arrayBuffer.isView", @@ -293,15 +293,15 @@ export function lowerBytesStaticCall( const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; if ( - call.arguments.length < 1 || - call.arguments.length > 3 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 3 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering("TypedArray.from with this argument count", call); } - const node = call.arguments[0]!; + const node = call.arguments.nodes[0]!; const source = lowerer.lowerExpr(node); - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { if (!["string", "array", "bytes"].includes(source.type.kind)) lowerer.noLowering("TypedArray.from mapper over this source representation", call); const loc = locOf(call); @@ -356,9 +356,9 @@ export function lowerBytesStaticCall( callee: key, args: [ lowerer.coerceInto(node, source, DYN), - lowerer.lowerExprExpecting(call.arguments[1]!, DYN), - call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], DYN) + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], DYN) : dynUndefinedExpr(loc), ], type: bytesOf(elem), @@ -414,7 +414,7 @@ function lowerArrayBufferView( } function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (!args.length || args.length > 3 || args.some(ts.isSpreadElement)) lowerer.noLowering("DataView constructor argument count", expr); return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); @@ -438,7 +438,7 @@ export function lowerBytesMethodCall( if (receiverIr?.kind !== "bytes") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - const nArgs = call.arguments.length; + const nArgs = call.arguments.nodes.length; // Encoded stream callbacks can receive strings even when JavaScript's // contextual inference still describes their parameter as a Buffer. if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) { @@ -451,8 +451,8 @@ export function lowerBytesMethodCall( fn: "dyn.toString", args: [ receiver, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), { kind: "strLit", value: access.getText(), type: STRING, loc }, ], @@ -492,7 +492,7 @@ export function lowerBytesMethodCall( }; } if (receiverIr.elem === "u8" && name === "with") { - if (nArgs !== 2 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.with with ${nArgs} arguments on Uint8Array`, call); } return { @@ -500,15 +500,15 @@ export function lowerBytesMethodCall( method: "with", receiver: lowerer.lowerExprExpecting(access.expression, receiverIr), args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, F64), - lowerer.lowerExprExpecting(call.arguments[1]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64), ], type: receiverIr, loc, }; } if (name === "join") { - if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.join with ${nArgs} arguments on typed arrays`, call); } const separatorDefault: IrExpr = { @@ -517,8 +517,8 @@ export function lowerBytesMethodCall( type: STRING, loc, }; - const separator = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], STRING, separatorDefault) + const separator = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], STRING, separatorDefault) : separatorDefault; return { kind: "bytesIntrinsic", @@ -536,7 +536,7 @@ export function lowerBytesMethodCall( const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); // A missing start is 0 and a missing end is the length (+Infinity // clamps to it), as for omitted arguments. - const args = call.arguments.map((a, i) => + const args = call.arguments.nodes.map((a, i) => lowerOptionalArgument(lowerer, a, F64, { kind: "numLit", value: i === 0 ? 0 : Infinity, @@ -563,13 +563,13 @@ export function lowerBytesMethodCall( return { kind: "bytesIntrinsic", method, receiver, args, type: receiverIr, loc }; } if (name === "copyWithin") { - if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.copyWithin with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const args = [0, 0, Infinity].map((value, i) => { const fallback: IrExpr = { kind: "numLit", value, type: F64, loc }; - const node = call.arguments[i]; + const node = call.arguments.nodes[i]; return node ? lowerOptionalArgument(lowerer, node, F64, fallback) : fallback; }); return { kind: "bytesIntrinsic", method: "copyWithin", receiver, args, type: receiverIr, loc }; @@ -592,15 +592,15 @@ export function lowerBytesMethodCall( ) { // Typed arrays use element coercion and relative, clamped indices. // Buffer's same-named method keeps its pattern and offset validation. - if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.fill with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc }; - const v = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], F64, zero) + const v = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], F64, zero) : zero; - const idx = call.arguments.slice(1).map((a, i) => + const idx = call.arguments.nodes.slice(1).map((a, i) => lowerOptionalArgument(lowerer, a, F64, { kind: "numLit", value: i === 0 ? 0 : Infinity, @@ -622,14 +622,14 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const src = lowerer.lowerExpr(call.arguments[0]!); + const src = lowerer.lowerExpr(call.arguments.nodes[0]!); const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn"; const args = [ - src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments[0]!, src, DYN), + src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN), ]; if (nArgs === 2) args.push( - lowerOptionalArgument(lowerer, call.arguments[1]!, F64, { + lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, { kind: "numLit", value: 0, type: F64, @@ -660,7 +660,7 @@ export function lowerBytesMethodCall( } if (nArgs > 3) lowerer.noLowering(`.toString with ${nArgs} arguments on Buffers`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const encNode = call.arguments[0]; + const encNode = call.arguments.nodes[0]; let method: IrBytesIntrinsicMethod = "toString"; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode) { @@ -698,9 +698,9 @@ export function lowerBytesMethodCall( // receiver's length, because EXPLICIT negative ends clamp to empty // in Node and no in-band sentinel can represent "omitted" safely. if (nArgs > 1) { - const start = lowerer.lowerExprExpecting(call.arguments[1]!, F64); + const start = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); if (nArgs === 3) { - const end = lowerer.lowerExprExpecting(call.arguments[2]!, F64); + const end = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, F64); return { kind: "bytesIntrinsic", method, @@ -741,7 +741,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offsetNode = call.arguments[required]; + const offsetNode = call.arguments.nodes[required]; const offset: IrExpr = offsetNode ? lowerer.lowerExprExpecting(offsetNode, F64) : { kind: "numLit", value: 0, type: F64, loc }; @@ -756,7 +756,7 @@ export function lowerBytesMethodCall( loc, }; } - const value = lowerer.lowerExprExpecting(call.arguments[0]!, BIGINT_T); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BIGINT_T); return { kind: "libCall", fn: "bigint.bufferWrite", @@ -785,7 +785,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: numKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; if (nArgs === required) { args.push({ kind: "numLit", value: 0, type: F64, loc }); // Node's offset default } @@ -814,7 +814,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: varKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; return { kind: "bytesIntrinsic", method: write ? "writeNumVar" : "readNumVar", @@ -833,10 +833,10 @@ export function lowerBytesMethodCall( if (dvBigGetter !== undefined && receiverIr.elem === "u8") { if (nArgs < 1 || nArgs > 2) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); const sign: IrExpr = { kind: "boolLit", value: dvBigGetter.sign, type: BOOL, loc }; - const le: IrExpr = call.arguments[1] - ? lowerer.lowerExprExpecting(call.arguments[1], BOOL) + const le: IrExpr = call.arguments.nodes[1] + ? lowerer.lowerExprExpecting(call.arguments.nodes[1], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -854,8 +854,8 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); // ToIndex(undefined) is ToIndex(NaN): a missing offset reads offset 0. - const args = [lowerToNumberArgument(lowerer, call.arguments[0]!)]; - if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, BOOL)); + const args = [lowerToNumberArgument(lowerer, call.arguments.nodes[0]!)]; + if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BOOL)); return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc }; } // DataView setters — the getters' mirror: (byteOffset, value) plus the @@ -868,10 +868,10 @@ export function lowerBytesMethodCall( if ((name === "setBigUint64" || name === "setBigInt64") && receiverIr.elem === "u8") { if (nArgs < 2 || nArgs > 3) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const value = lowerer.lowerExprExpecting(call.arguments[1]!, BIGINT_T); - const le: IrExpr = call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], BOOL) + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BIGINT_T); + const le: IrExpr = call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -890,10 +890,10 @@ export function lowerBytesMethodCall( // The offset (ToIndex) and the value (ToNumber) treat a missing value // as NaN. const args = [ - lowerToNumberArgument(lowerer, call.arguments[0]!), - lowerToNumberArgument(lowerer, call.arguments[1]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[0]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[1]!), ]; - if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments[2]!, BOOL)); + if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[2]!, BOOL)); return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc }; } return null; @@ -984,7 +984,7 @@ function lowerBufferInstanceMethod( name: string, loc: SrcLoc, ): IrExpr | null { - const args = call.arguments; + const args = call.arguments.nodes; const nArgs = args.length; if (args.some(ts.isSpreadElement)) return null; const isStringArg = (i: number): boolean => @@ -1041,10 +1041,12 @@ function lowerBufferInstanceMethod( const fast = nArgs >= 1 && argIrKind(0)?.kind === "bytes" && - call.arguments.slice(1).every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); + call.arguments.nodes + .slice(1) + .every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); if (fast) { const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1121,7 +1123,7 @@ function lowerBufferInstanceMethod( if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.fill with ${nArgs} arguments`, call); // A trailing string-typed arg past the value is the encoding. let encNode: ts.Expression | undefined; - let idxNodes = call.arguments.slice(1); + let idxNodes = call.arguments.nodes.slice(1); if (idxNodes.length > 0 && isStringArg(nArgs - 1)) { encNode = idxNodes[idxNodes.length - 1]; idxNodes = idxNodes.slice(0, -1); @@ -1171,7 +1173,7 @@ function lowerBufferInstanceMethod( if (name === "copy") { if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.copy with ${nArgs} arguments`, call); const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1192,7 +1194,7 @@ function lowerBufferInstanceMethod( // (str), (str, enc), (str, offset), (str, offset, enc), // (str, offset, length), (str, offset, length, enc). let encNode: ts.Expression | undefined; - let idxNodes = call.arguments.slice(1); + let idxNodes = call.arguments.nodes.slice(1); if (idxNodes.length > 0 && isStringArg(nArgs - 1)) { encNode = idxNodes[idxNodes.length - 1]; idxNodes = idxNodes.slice(0, -1); @@ -1299,7 +1301,7 @@ function knownBufferProducer( if ( !ts.isPropertyAccessExpression(callee) || callee.name.text !== "digest" || - node.arguments.length !== 0 + node.arguments.nodes.length !== 0 ) return false; const type = lowerer.mapTypeOf(lowerer.typeOf(callee.expression)); @@ -1328,7 +1330,7 @@ function lowerBufferStaticValue( if (!lowerer.isStdlibGlobal(access.expression, "Buffer")) return null; const member = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (member === "from") { if ( args.length >= 1 && @@ -1371,11 +1373,14 @@ function lowerBufferStaticValue( } if (args.length >= 1 && args.length <= 2 && !args.some(ts.isSpreadElement)) { const argNode = args[0]!; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { + if ( + ts.isArrayLiteralExpression(argNode) && + !argNode.elements.nodes.some(ts.isSpreadElement) + ) { // A number[] literal (contextually typed by the lib's readonly // number[] slot — build it element-wise, the Set-seed pattern). if (args.length === 1) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; } @@ -1521,8 +1526,8 @@ function lowerBufferStaticValue( } const argNode = args[0]!; let list: IrExpr; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, BYTES_U8)); + if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.nodes.some(ts.isSpreadElement)) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, BYTES_U8)); list = { kind: "arrayLit", elems, type: arrayOf(BYTES_U8), loc }; } else { list = lowerer.lowerExpr(argNode); diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 96147cbe5..e87a5e1bc 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -56,7 +56,7 @@ export function lowerArrayCallback( lead[0]?.kind === "dyn" && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) ) { - for (const p of argNode.parameters) { + for (const p of argNode.parameters.nodes) { if (!ts.isIdentifier(p.name) || p.type || p.initializer || p.dotDotDotToken) continue; const t = lowerer.checker.getTypeAtLocation(p.name); if ((t.flags & ts.TypeFlags.Any) !== 0 && !lowerer.chainNarrowedType.has(p.name)) { @@ -71,7 +71,7 @@ export function lowerArrayCallback( // handles the loop passes, whatever the contextual type spelled — if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((_p, _i) => { + argNode.parameters.nodes.forEach((_p, _i) => { return; }); } @@ -85,7 +85,7 @@ export function lowerArrayCallback( const previousImplicit = lowerer.implicitParamTypes; let contextualTs: Map | null = null; if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((param, i) => { + argNode.parameters.nodes.forEach((param, i) => { const expected = full[i]; if ( expected === undefined || @@ -137,7 +137,7 @@ export function lowerArrayCallback( // that stored layout; its names then see the optional values. const patterns: { node: ts.Node; previous: IrType | undefined }[] = []; if (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode)) { - argNode.parameters.forEach((param, i) => { + argNode.parameters.nodes.forEach((param, i) => { const expected = full[i]; if ( expected === undefined || diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index b6dec2913..7c4ddf220 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -48,20 +48,23 @@ export function lowerGroupByStaticCall( if (!lowerer.isStdlibMember(access)) return null; const what = isMap ? "Map.groupBy" : "Object.groupBy"; const loc = locOf(call); - if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) { - lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call); + if ( + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + lowerer.noLowering(`${what} with ${call.arguments.nodes.length} arguments`, call); } - const items = lowerer.lowerExpr(call.arguments[0]!); + const items = lowerer.lowerExpr(call.arguments.nodes[0]!); const elem = groupedElement(lowerer, items.type); if (elem === null) { lowerer.noLowering( - `${what} over an iterable of type '${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))}'`, - call.arguments[0]!, + `${what} over an iterable of type '${lowerer.checker.typeToString(lowerer.typeOf(call.arguments.nodes[0]!))}'`, + call.arguments.nodes[0]!, "arrays, Sets, Maps and generators are the supported items forms — spread other iterables first: [...items]", ); } const groupT = arrayOf(elem); - const fNode = call.arguments[1]!; + const fNode = call.arguments.nodes[1]!; // An unannotated JavaScript callback parameter takes the element. const f = isJsSourceFile(fNode.getSourceFile()) && @@ -74,7 +77,7 @@ export function lowerGroupByStaticCall( (f.type.params.length >= 1 && !typeEquals(f.type.params[0]!, elem)) || (f.type.params.length === 2 && f.type.params[1]!.kind !== "f64") ) { - lowerer.badType(call.arguments[1]!, lowerer.typeOf(call.arguments[1]!)); + lowerer.badType(call.arguments.nodes[1]!, lowerer.typeOf(call.arguments.nodes[1]!)); } const keyT = f.type.ret; // The call's own type keeps the callback's literal-union key type diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 8c8349c0b..a8b782b51 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -80,16 +80,16 @@ export function lowerMapMethodCall( ); } // The lib declares a thisArg parameter on forEach; unlowered — fenced. - if (name === "forEach" && call.arguments.length !== 1) { + if (name === "forEach" && call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.forEach with ${call.arguments.length} arguments`, + `.forEach with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); } if (name === "get") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); // The checker types the call `V | undefined`, which interns the // result union. `undefined` sorts LAST among all possible arm // typeKeys, so when V is itself a union its arms keep their tags in @@ -109,11 +109,11 @@ export function lowerMapMethodCall( return { kind: "mapIntrinsic", method: "get", receiver, args: [k], type, loc }; } if (name === "set") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); const v = receiverIr.value.kind === "dyn" - ? lowerer.lowerCollectionKey(call.arguments[1]!, receiverIr.value) - : lowerer.lowerExprExpecting(call.arguments[1]!, receiverIr.value); + ? lowerer.lowerCollectionKey(call.arguments.nodes[1]!, receiverIr.value) + : lowerer.lowerExprExpecting(call.arguments.nodes[1]!, receiverIr.value); // A discarded set result needs no second receiver read or identity // owner. Keep the ordinary operand order: receiver, key, then value. let parent: ts.Node | undefined = call.parent; @@ -146,7 +146,7 @@ export function lowerMapMethodCall( }; } if (name === "has" || name === "delete") { - const k = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.key); + const k = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.key); return { kind: "mapIntrinsic", method: name, receiver, args: [k], type: BOOL, loc }; } if (name === "clear") { @@ -452,14 +452,14 @@ export function lowerMapForEachCall( bindUntyped = false, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const previousRuntime: { symbol: ts.Symbol; type: IrType | undefined }[] = []; const previousImplicit = lowerer.implicitParamTypes; let contextualTs: Map | null = null; if (bindUntyped && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode))) { const expected = [mapT.value, mapT.key]; - argNode.parameters.slice(0, 2).forEach((param, i) => { + argNode.parameters.nodes.slice(0, 2).forEach((param, i) => { const type = expected[i]; if ( type === undefined || @@ -567,9 +567,9 @@ export function lowerSetMethodCall( "the lowered add() produces no value — call each add(v) as its own statement", ); } - if (name === "forEach" && call.arguments.length !== 1) { + if (name === "forEach" && call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.forEach with ${call.arguments.length} arguments`, + `.forEach with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); @@ -581,11 +581,11 @@ export function lowerSetMethodCall( return lowerSetCombineCall(lowerer, call, name, receiver, receiverIr); } if (name === "add") { - const v = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.elem); + const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); return { kind: "setIntrinsic", method: "add", receiver, args: [v], type: VOID, loc }; } if (name === "has" || name === "delete") { - const v = lowerer.lowerCollectionKey(call.arguments[0]!, receiverIr.elem); + const v = lowerer.lowerCollectionKey(call.arguments.nodes[0]!, receiverIr.elem); return { kind: "setIntrinsic", method: name, receiver, args: [v], type: BOOL, loc }; } if (name === "clear") { @@ -606,7 +606,7 @@ export function lowerSetForEachCall( setT: IrType & { kind: "set" }, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const fnArg = lowerer.lowerExpr(argNode); if ( @@ -644,10 +644,10 @@ function lowerSetCombineCall( setT: IrType & { kind: "set" }, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { lowerer.noLowering( - `Set.prototype.${name} with ${call.arguments.length} arguments`, + `Set.prototype.${name} with ${call.arguments.nodes.length} arguments`, call, "exactly one Set argument is the supported form", ); @@ -918,8 +918,10 @@ export function lowerSetSeedNew( } // Constructor contextual types include Iterable, which has no value // representation. Build a literal at the actual Set element ABI instead. - if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) { - const elems = node.elements.map((element) => lowerer.lowerCollectionKey(element, setT.elem)); + if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) { + const elems = node.elements.nodes.map((element) => + lowerer.lowerCollectionKey(element, setT.elem), + ); return { kind: "setNew", seed: { kind: "arrayLit", elems, type: arrayOf(setT.elem), loc }, @@ -1154,7 +1156,7 @@ export function lowerMapSeedNew( if (ts.isArrayLiteralExpression(argNode)) { if ( isJsSourceFile(argNode.getSourceFile()) && - argNode.elements.some((entry) => !ts.isArrayLiteralExpression(entry)) + argNode.elements.nodes.some((entry) => !ts.isArrayLiteralExpression(entry)) ) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); } diff --git a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts index cb1978e51..f2967c298 100644 --- a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts @@ -23,7 +23,7 @@ export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression) const statement = decl.parent.parent; if ( ts.isVariableStatement(statement) && - statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ) return null; const symbol = lowerer.resolveValueSymbol(decl.name); diff --git a/packages/compiler/src/frontend/lowering/containers/for-of.ts b/packages/compiler/src/frontend/lowering/containers/for-of.ts index 6aa838dea..79f970eb1 100644 --- a/packages/compiler/src/frontend/lowering/containers/for-of.ts +++ b/packages/compiler/src/frontend/lowering/containers/for-of.ts @@ -77,7 +77,7 @@ export function lowerForOfSearchParams( } const isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one lowerer.scopes.push(new Map()); try { const sp = lowerer.declareHiddenLocal("%spof", SP); @@ -108,13 +108,15 @@ export function lowerForOfSearchParams( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl.name) && - decl.name.elements.length >= 1 && - decl.name.elements.length <= 2 && - decl.name.elements.every(isPlainIdent) + decl.name.elements.nodes.length >= 1 && + decl.name.elements.nodes.length <= 2 && + decl.name.elements.nodes.every(isPlainIdent) ) { // `for (const [k, v] of sp)` — bind straight from the reads; the // tuple never exists. `[k]` alone reads only the key. - const els = decl.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, STRING, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { @@ -260,7 +262,7 @@ export function lowerForOfArrayIter( } const isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one lowerer.scopes.push(new Map()); try { const arr = lowerer.declareHiddenLocal("%arof", arrT); @@ -293,13 +295,15 @@ export function lowerForOfArrayIter( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl.name) && - decl.name.elements.length >= 1 && - decl.name.elements.length <= 2 && - decl.name.elements.every(isPlainIdent) + decl.name.elements.nodes.length >= 1 && + decl.name.elements.nodes.length <= 2 && + decl.name.elements.nodes.every(isPlainIdent) ) { // `for (const [i, v] of xs.entries())` — bind straight from the // reads; the tuple never exists. `[i]` alone reads only the index. - const els = decl.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, F64, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { @@ -493,7 +497,7 @@ function lowerForOfMapOrSet( } isVar = (list.flags & ts.NodeFlags.BlockScoped) === 0; isLet = (list.flags & ts.NodeFlags.Let) !== 0 || isVar; - decl = list.declarations[0]!; // the grammar allows exactly one + decl = list.declarations.nodes[0]!; // the grammar allows exactly one } lowerer.scopes.push(new Map()); try { @@ -603,13 +607,15 @@ function lowerForOfMapOrSet( yieldsPair && !isVar && // var pattern names assign hoisted slots — the generic desugar below owns them ts.isArrayBindingPattern(decl!.name) && - decl!.name.elements.length >= 1 && - decl!.name.elements.length <= 2 && - decl!.name.elements.every(isPlainIdent) + decl!.name.elements.nodes.length >= 1 && + decl!.name.elements.nodes.length <= 2 && + decl!.name.elements.nodes.every(isPlainIdent) ) { // `for (const [k, v] of m)` — bind straight from the primitives; // the tuple never exists. `[k]` alone reads only the key. - const els = decl!.name.elements as readonly (ts.BindingElement & { name: ts.Identifier })[]; + const els = decl!.name.elements.nodes as readonly (ts.BindingElement & { + name: ts.Identifier; + })[]; const kLocal = lowerer.declareLocal(els[0]!.name, els[0]!.name.text, keyT, isLet); binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc }); if (els[1]) { diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index cca4bd51b..38df368ac 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -92,7 +92,12 @@ export function lowerObjectIterOverIndexShape( member, argIr, shape, - objectEnumerationReceiver(lowerer, lowerer.lowerExpr(call.arguments[0]!), argIr, locOf(call)), + objectEnumerationReceiver( + lowerer, + lowerer.lowerExpr(call.arguments.nodes[0]!), + argIr, + locOf(call), + ), resultT, locOf(call), ); @@ -441,8 +446,8 @@ export function lowerObjectFromEntriesCall( callee.questionDotToken || !lowerer.isStdlibGlobal(callee.expression, "Object") || callee.name.text !== "fromEntries" || - call.arguments.length !== 1 || - ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length !== 1 || + ts.isSpreadElement(call.arguments.nodes[0]!) ) return null; @@ -451,7 +456,7 @@ export function lowerObjectFromEntriesCall( fn: "dyn.fromEntries", type: DYN, loc: locOf(call), - args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], }; } @@ -464,8 +469,9 @@ function lowerTypedObjectFromEntriesCall( if (call.questionDotToken || callee.questionDotToken) return null; if (!lowerer.isStdlibGlobal(callee.expression, "Object")) return null; if (callee.name.text !== "fromEntries") return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const argNode = call.arguments.nodes[0]!; const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (argIr?.kind !== "array") return null; const represented = tryLowerExpression(lowerer, argNode); @@ -1052,9 +1058,10 @@ export function lowerObjectAssignIndexShape( lowerer: Lowerer, call: ts.CallExpression, ): IrExpr | null { - if (call.arguments.length < 2 || call.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (call.arguments.nodes.length < 2 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + return null; const loc = locOf(call); - const targetIr = lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!)); + const targetIr = lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!)); if (targetIr?.kind !== "record") return null; const to = lowerer.shapes.get(targetIr.shapeId); if (!to?.indexValue || to.tuple) return null; @@ -1084,7 +1091,7 @@ export function lowerObjectAssignIndexShape( ovfLift: (WidthLift | "dyn") | null; } const plans: SourcePlan[] = []; - for (const argNode of call.arguments.slice(1)) { + for (const argNode of call.arguments.nodes.slice(1)) { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (srcIr?.kind !== "record") return null; const from = lowerer.shapes.get(srcIr.shapeId); @@ -1252,9 +1259,9 @@ export function lowerObjectAssignIndexShape( lowerer.liftedFns.push(fn); return name; }; - let acc = lowerer.lowerExprExpecting(call.arguments[0]!, targetIr); + let acc = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, targetIr); for (let i = 0; i < plans.length; i++) { - const src = lowerer.lowerExprExpecting(call.arguments[i + 1]!, { + const src = lowerer.lowerExprExpecting(call.arguments.nodes[i + 1]!, { kind: "record", shapeId: plans[i]!.fromId, }); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 79b91af64..aeefffe48 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -81,7 +81,7 @@ export function lowerIteratorHelperCall( if (!ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return null; if (cur.questionDotToken || cur.expression.questionDotToken) return null; const name = cur.expression.name.text; - if ((name === "values" || name === "entries") && cur.arguments.length === 0) { + if ((name === "values" || name === "entries") && cur.arguments.nodes.length === 0) { const recvIr = lowerer.mapTypeOf(lowerer.typeOf(cur.expression.expression)); if (recvIr?.kind !== "array") return null; if (!lowerer.isStdlibMember(cur.expression)) return null; @@ -91,11 +91,11 @@ export function lowerIteratorHelperCall( } if ( !ITER_STAGES.has(name) || - cur.arguments.length !== 1 || - ts.isSpreadElement(cur.arguments[0]!) + cur.arguments.nodes.length !== 1 || + ts.isSpreadElement(cur.arguments.nodes[0]!) ) return null; - stages.push({ name, argNode: cur.arguments[0]!, access: cur.expression }); + stages.push({ name, argNode: cur.arguments.nodes[0]!, access: cur.expression }); cur = cur.expression.expression; } stages.reverse(); // source order @@ -154,9 +154,9 @@ export function lowerGeneratorHelperCall( if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); if (!isTerminal) { - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { - lowerer.noLowering(`.${name} with ${call.arguments.length} arguments`, call); + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${name} with ${call.arguments.nodes.length} arguments`, call); } // A stage on m.keys()/values()/entries() (or a Set's) walks the // collection inside the helper's own generator. @@ -205,7 +205,11 @@ export function lowerGeneratorHelperCall( let cur: ts.Expression = access.expression; while (isStageCall(lowerer, cur)) { const stageAccess = cur.expression as ts.PropertyAccessExpression; - stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + stages.push({ + name: stageAccess.name.text, + argNode: cur.arguments.nodes[0]!, + access: stageAccess, + }); cur = stageAccess.expression; } stages.reverse(); // source order @@ -271,7 +275,7 @@ export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression) if (nonNull) callee = (callee as ts.NonNullExpression).expression; if (!nonNull && call.questionDotToken === undefined) return null; if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; - if (callee.name.text !== "return" || call.arguments.length !== 0) return null; + if (callee.name.text !== "return" || call.arguments.nodes.length !== 0) return null; if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null; const callT = lowerer.mapTypeOf(lowerer.typeOf(call)); const gen = lowerer.lowerExpr(callee.expression); @@ -321,8 +325,8 @@ export function lowerIteratorFromCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (access.name.text !== "from" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (access.name.text !== "from" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (ts.isSpreadElement(argNode)) return null; const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); @@ -409,7 +413,11 @@ export function lowerForOfHelperChain( while (isStageCall(lowerer, cur)) { const stageAccess = cur.expression as ts.PropertyAccessExpression; if (stageAccess.name.text === "flatMap") return null; - stages.push({ name: stageAccess.name.text, argNode: cur.arguments[0]!, access: stageAccess }); + stages.push({ + name: stageAccess.name.text, + argNode: cur.arguments.nodes[0]!, + access: stageAccess, + }); cur = stageAccess.expression; } if (stages.length === 0) return null; @@ -533,9 +541,13 @@ export function lowerForOfHelperChain( * argument directly. */ function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null { if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 1 + ) return null; - const arg = expr.arguments[0]!; + const arg = expr.arguments.nodes[0]!; if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null; if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null; return lowerer.isStdlibMember(expr.expression) ? arg : null; @@ -553,7 +565,11 @@ function arrayIteratorCall( ? { receiver: fromArg, proj: "values" } : null; if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "values" && proj !== "entries") return null; @@ -606,7 +622,11 @@ function collectionIteratorCall( : null; } if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "keys" && proj !== "values" && proj !== "entries") return null; @@ -668,7 +688,8 @@ function isStageCall(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpr if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return false; if (expr.questionDotToken || expr.expression.questionDotToken) return false; if (!ITER_STAGES.has(expr.expression.name.text)) return false; - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return false; + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + return false; return ( isHelperReceiver(lowerer, expr.expression.expression) && lowerer.isStdlibMember(expr.expression) ); @@ -817,8 +838,8 @@ function lowerTerminal( ): { termArgs: IrExpr[]; resultT: IrType } { const termArgs: IrExpr[] = []; if (terminal === "toArray") { - if (call.arguments.length !== 0) - lowerer.noLowering(`.toArray with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`.toArray with ${call.arguments.nodes.length} arguments`, call); return { termArgs, resultT: arrayOf(elem) }; } if ( @@ -827,9 +848,9 @@ function lowerTerminal( terminal === "every" || terminal === "find" ) { - const argNode = call.arguments[0]; - if (call.arguments.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { - lowerer.noLowering(`.${terminal} with ${call.arguments.length} arguments`, call); + const argNode = call.arguments.nodes[0]; + if (call.arguments.nodes.length !== 1 || argNode === undefined || ts.isSpreadElement(argNode)) { + lowerer.noLowering(`.${terminal} with ${call.arguments.nodes.length} arguments`, call); } const f = lowerArrayCallback(lowerer, argNode, [elem], UNDEFINED_T).fnArg; if ( @@ -849,14 +870,14 @@ function lowerTerminal( }; } // reduce(f[, initial]) - const fNode = call.arguments[0]; + const fNode = call.arguments.nodes[0]; if ( - call.arguments.length < 1 || - call.arguments.length > 2 || + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || fNode === undefined || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - lowerer.noLowering(`.reduce with ${call.arguments.length} arguments`, call); + lowerer.noLowering(`.reduce with ${call.arguments.nodes.length} arguments`, call); } // The accumulator binds the checker's result type. const accLead = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? elem; @@ -872,7 +893,7 @@ function lowerTerminal( lowerer.badType(fNode, lowerer.typeOf(fNode)); } const accT = (f.type as IrType & { kind: "func" }).ret; - if (call.arguments.length === 1 && !typeEquals(accT, elem)) { + if (call.arguments.nodes.length === 1 && !typeEquals(accT, elem)) { lowerer.noLowering( ".reduce without an initial value where the accumulator's type differs from the elements'", call, @@ -880,8 +901,8 @@ function lowerTerminal( ); } termArgs.push(f); - if (call.arguments[1] !== undefined) - termArgs.push(lowerer.lowerExprExpecting(call.arguments[1], accT)); + if (call.arguments.nodes[1] !== undefined) + termArgs.push(lowerer.lowerExprExpecting(call.arguments.nodes[1], accT)); return { termArgs, resultT: accT }; } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts index 387ff81d3..f936e27cb 100644 --- a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -21,14 +21,14 @@ export function matchAllRowsReadByElements( const parent = outerValue(drain).parent; if ( ts.isCallExpression(parent) && - parent.arguments[0] === outerValue(drain) && + parent.arguments.nodes[0] === outerValue(drain) && ts.isPropertyAccessExpression(parent.expression) && parent.expression.name.text === "from" && lowerer.isStdlibGlobal(parent.expression.expression, "Array") ) { - const mapper = parent.arguments[1]; + const mapper = parent.arguments.nodes[1]; if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0); - return parent.arguments.length === 2 && callbackReadsByElements(lowerer, mapper, 2); + return parent.arguments.nodes.length === 2 && callbackReadsByElements(lowerer, mapper, 2); } if (ts.isForOfStatement(parent) && parent.expression === outerValue(drain)) return forOfReadsByElements(lowerer, parent, companion); @@ -103,7 +103,7 @@ function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boo const literal = parent.parent; return ( ts.isArrayLiteralExpression(literal) && - literal.elements.length === 1 && + literal.elements.nodes.length === 1 && rowsReadByElements(lowerer, literal, depth + 1) ); } @@ -122,10 +122,13 @@ function rowsReadByElements(lowerer: Lowerer, node: ts.Node, depth: number): boo if (!ts.isCallExpression(call) || call.expression !== parent) return method === "length" && isValueRead(parent); if (ROW_CALLBACK_METHODS.has(method)) { - if (call.arguments.length !== 1 || !callbackReadsByElements(lowerer, call.arguments[0]!, 2)) + if ( + call.arguments.nodes.length !== 1 || + !callbackReadsByElements(lowerer, call.arguments.nodes[0]!, 2) + ) return false; } else if (ROW_SUBSET_METHODS.has(method) || ROW_PICK_METHODS.has(method)) { - if (call.arguments.some((a) => ts.isSpreadElement(a))) return false; + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) return false; } else { return false; } @@ -172,8 +175,8 @@ function forOfReadsByElements( companion: boolean, ): boolean { const list = loop.initializer; - if (!ts.isVariableDeclarationList(list) || list.declarations.length !== 1) return false; - const decl = list.declarations[0]!; + if (!ts.isVariableDeclarationList(list) || list.declarations.nodes.length !== 1) return false; + const decl = list.declarations.nodes[0]!; if (!ts.isIdentifier(decl.name)) return false; const symbol = lowerer.resolveValueSymbol(decl.name); if (!symbol || !bindingNeverReassigned(lowerer, symbol, decl)) return false; @@ -202,8 +205,8 @@ function callbackReadsByElements( let fn: ts.Node = node; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; - if (fn.parameters.length > maxParams) return false; - const param = fn.parameters[0]; + if (fn.parameters.nodes.length > maxParams) return false; + const param = fn.parameters.nodes[0]; if (param === undefined) return true; if (param.dotDotDotToken !== undefined || param.initializer !== undefined) return false; if (ts.isObjectBindingPattern(param.name)) @@ -230,7 +233,7 @@ function constBindingReadsByElements( const statement = decl.parent.parent; if ( ts.isVariableStatement(statement) && - statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) + statement.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ) return false; const symbol = lowerer.resolveValueSymbol(decl.name); @@ -267,8 +270,8 @@ function groupsProjectedParam(lowerer: Lowerer, param: ts.ParameterDeclaration): /** An object pattern that binds only `groups`. */ function groupsPattern(pattern: ts.ObjectBindingPattern): boolean { return ( - pattern.elements.length > 0 && - pattern.elements.every((el) => { + pattern.elements.nodes.length > 0 && + pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined) return false; const prop = el.propertyName; if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; 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 dd70eb197..832dc7def 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -738,7 +738,7 @@ export function lowerRegexMethodCall( }; const loc = locOf(call); if (receiverKind === "regex" && name === "toString") { - if (call.arguments.length !== 0) { + if (call.arguments.nodes.length !== 0) { lowerer.noLowering("RegExp.prototype.toString with arguments", call); } return { @@ -751,20 +751,21 @@ export function lowerRegexMethodCall( }; } if (receiverKind === "regex" && name === "test") { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("RegExp.prototype.test with surplus or spread arguments", call); } const receiver = lowerReceiver(); - const args = [lowerRegexSubject(lowerer, call.arguments[0], loc)]; + const args = [lowerRegexSubject(lowerer, call.arguments.nodes[0], loc)]; return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc }; } // `re.exec(s)` returns the exec-shaped string[] | null slice, with // numeric lastIndex state for global/sticky regexes. Nonparticipating // captures retain match's present undefined element state. if (receiverKind === "regex" && name === "exec") { - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; const re = lowerReceiver(); - const subject = lowerRegexSubject(lowerer, call.arguments[0], loc); + const subject = lowerRegexSubject(lowerer, call.arguments.nodes[0], loc); const resultT: IrType = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]), @@ -819,9 +820,9 @@ export function lowerRegexMethodCall( ); })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); @@ -863,9 +864,9 @@ export function lowerRegexMethodCall( // regexes throw Node's exact TypeError at runtime, catchably // (replaceAll's stance). if (receiverKind === "string" && name === "matchAll") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { @@ -882,9 +883,9 @@ export function lowerRegexMethodCall( // — a fresh exec from position 0, so /g is irrelevant and /y anchors at // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence - if (call.arguments.length !== 1) return null; + if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; @@ -897,9 +898,9 @@ export function lowerRegexMethodCall( receiverKind === "string" && (name === "replace" || name === "replaceAll" || name === "split") ) { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1120", call, @@ -913,11 +914,13 @@ export function lowerRegexMethodCall( name === "split" ? [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), - lowerSplitLimitArg(lowerer, call.arguments[1], loc), + lowerSplitLimitArg(lowerer, call.arguments.nodes[1], loc), ] : [ lowerer.lowerExprExpecting(arg0, { kind: "regex" }), - call.arguments[1] ? lowerer.lowerExpr(call.arguments[1]) : dynUndefinedExpr(loc), + call.arguments.nodes[1] + ? lowerer.lowerExpr(call.arguments.nodes[1]) + : dynUndefinedExpr(loc), ]; if (name !== "split") { const replacement = args[1]; @@ -946,14 +949,14 @@ export function lowerRegexMethodCall( if (!replacement || !templateType(replacement.type)) { lowerer.unsupported( "SC1120", - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "replacement values that are not callable or convertible to a string", ); } // Array iteration can carry an explicit undefined even when the // checker spells string. RegExp replacement applies ToString once, // including when the pattern has no match; it does not call that value. - args[1] = coerceStringSearchValue(lowerer, replacement, call.arguments[1]!, loc); + args[1] = coerceStringSearchValue(lowerer, replacement, call.arguments.nodes[1]!, loc); } return { kind: "regexIntrinsic", @@ -983,10 +986,10 @@ function lowerReplaceCharChain(lowerer: Lowerer, call: ts.CallExpression): IrExp !ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined || (callee.name.text !== "replace" && callee.name.text !== "replaceAll") || - node.arguments.length !== 2 + node.arguments.nodes.length !== 2 ) break; - const [pattern, replacement] = node.arguments; + const [pattern, replacement] = node.arguments.nodes; if ( !pattern || !ts.isRegularExpressionLiteral(pattern) || @@ -1046,7 +1049,7 @@ export function lowerStringMethodCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, dynReceiver?: () => IrExpr, - argumentNodes: readonly ts.Expression[] = call.arguments, + argumentNodes: readonly ts.Expression[] = call.arguments.nodes, ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; if (dynReceiver === undefined && access.name.text === "localeCompare") @@ -1369,17 +1372,17 @@ function lowerLocaleCompareCall( if (receiverIr?.kind !== "string") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.localeCompare with ${call.arguments.length} arguments`, + `.localeCompare with ${call.arguments.nodes.length} arguments`, call, "the locales/options parameters select ICU collations that have no lowering — " + "pass exactly the comparison string", ); } const receiver = lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text); - const arg = lowerer.lowerExpr(call.arguments[0]!); + const arg = lowerer.lowerExpr(call.arguments.nodes[0]!); if (arg.type.kind !== "string") - lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!)); + lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!)); return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc }; } diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 1fdf09ef4..6856bc50b 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -155,7 +155,7 @@ function lowerStringReplacementValues( ) { lowerer.noLowering( `string .${method} callback signature`, - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "use a callback returning string with a prefix of (match: string, offset: number, subject: string)", ); } diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index fb7bc7abc..6b0b54b55 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -23,8 +23,11 @@ export class DeferredModuleInitializers { defer(lowerer: Lowerer, statement: ts.Statement): IrStmt | null { let key: string; - if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length === 1) { - const declaration = statement.declarationList.declarations[0]!; + if ( + ts.isVariableStatement(statement) && + statement.declarationList.declarations.nodes.length === 1 + ) { + const declaration = statement.declarationList.declarations.nodes[0]!; if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return null; const symbol = lowerer.checker.getSymbolAtLocation(declaration.name); const global = symbol && lowerer.globalsBySymbol.get(symbol); @@ -34,7 +37,7 @@ export class DeferredModuleInitializers { key = global.id; if ( ts.isObjectLiteralExpression(declaration.initializer) && - declaration.initializer.properties.every( + declaration.initializer.properties.nodes.every( (property) => (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name) && @@ -260,11 +263,11 @@ function registryInitializer( if ( !ts.isPropertyAccessExpression(expression.expression) || lowerer.stdlibGlobalMember(expression.expression, "Object") !== "freeze" || - expression.arguments.length !== 1 || - !ts.isObjectLiteralExpression(expression.arguments[0]!) + expression.arguments.nodes.length !== 1 || + !ts.isObjectLiteralExpression(expression.arguments.nodes[0]!) ) return false; - expression = expression.arguments[0]!; + expression = expression.arguments.nodes[0]!; } const packageSource = npmStaticPackageOfPath(statement.getSourceFile().fileName) !== null; const initialized = (declaration: ts.Node): boolean => { @@ -324,7 +327,7 @@ function registryInitializer( const trivia = node.getSourceFile().text.slice(node.pos, node.getStart()); if (!/[@#]__PURE__/.test(trivia)) return false; if (!callable(node.expression)) return false; - return (node.arguments ?? []).every((argument) => value(argument, depth + 1)); + return (node.arguments?.nodes ?? []).every((argument) => value(argument, depth + 1)); } if ( ts.isPropertyAccessExpression(node) && @@ -358,11 +361,11 @@ function registryInitializer( ); } if (ts.isArrayLiteralExpression(node)) - return node.elements.every( + return node.elements.nodes.every( (element) => !ts.isSpreadElement(element) && value(element, depth + 1), ); if (ts.isObjectLiteralExpression(node)) - return node.properties.every((property) => { + return node.properties.nodes.every((property) => { if ( ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || @@ -459,11 +462,11 @@ function registryOwnerIsData( ts.isCallExpression(initializer) && ts.isPropertyAccessExpression(initializer.expression) && lowerer.stdlibGlobalMember(initializer.expression, "Object") === "freeze" && - initializer.arguments.length === 1; - if (frozen) initializer = (initializer as ts.CallExpression).arguments[0]!; + initializer.arguments.nodes.length === 1; + if (frozen) initializer = (initializer as ts.CallExpression).arguments.nodes[0]!; if ( !ts.isObjectLiteralExpression(initializer) || - !initializer.properties.every( + !initializer.properties.nodes.every( (property) => ts.isShorthandPropertyAssignment(property) || (ts.isPropertyAssignment(property) && @@ -527,7 +530,7 @@ function classWriteIsUnobserved( const source = statement.getSourceFile(); const declaration = info.decl!; if ( - declaration.members.some( + declaration.members.nodes.some( (member) => ts.isClassStaticBlockDeclaration(member) || (ts.isPropertyDeclaration(member) && @@ -539,7 +542,7 @@ function classWriteIsUnobserved( ) ) return false; - for (const earlier of source.statements) { + for (const earlier of source.statements.nodes) { if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart()) continue; if ( @@ -551,7 +554,7 @@ function classWriteIsUnobserved( continue; if ( ts.isVariableStatement(earlier) && - earlier.declarationList.declarations.every( + earlier.declarationList.declarations.nodes.every( (binding) => !binding.initializer || registryInitializer(lowerer, binding.initializer, earlier), ) diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index b71080a3f..13f3f2dbd 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -249,14 +249,14 @@ function runtimeProvided(info: ClassInfo): boolean { function exportedNames(files: readonly ts.SourceFile[]): ReadonlySet { const names = new Set(); for (const sf of files) - for (const statement of sf.statements) { + for (const statement of sf.statements.nodes) { if ( ts.isExportDeclaration(statement) && statement.moduleSpecifier === undefined && statement.exportClause && ts.isNamedExports(statement.exportClause) ) { - for (const element of statement.exportClause.elements) + for (const element of statement.exportClause.elements.nodes) names.add((element.propertyName ?? element.name).text); } else if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)) { names.add(statement.expression.text); diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 2a9009aed..ceea9a3e3 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -67,7 +67,7 @@ export function scanDivergences( node.body !== undefined && node.name !== undefined && ts.isIdentifier(node.name) && - node.parameters.length > 0 + node.parameters.nodes.length > 0 ) { const list = fnsByName.get(node.name.text) ?? []; list.push(node); @@ -78,7 +78,8 @@ export function scanDivergences( const callee = node.expression; if (ts.isPropertyAccessExpression(callee)) { const member = callee.name.text; - if (member === "localeCompare" && node.arguments.length === 1) localeCompares.push(node); + if (member === "localeCompare" && node.arguments.nodes.length === 1) + localeCompares.push(node); const recv = callee.expression; if (ts.isIdentifier(recv)) { if ( @@ -86,7 +87,7 @@ export function scanDivergences( (recv.text === "Object" && (member === "keys" || member === "values" || member === "entries")) ) { - if (node.arguments[0] !== undefined) observers.push(node.arguments[0]); + if (node.arguments.nodes[0] !== undefined) observers.push(node.arguments.nodes[0]); } else if ( recv.text === "console" && (member === "log" || @@ -96,9 +97,10 @@ export function scanDivergences( member === "debug" || member === "dir") ) { - for (const a of node.arguments) if (!isLiteralLike(a)) observers.push(a); + for (const a of node.arguments.nodes) if (!isLiteralLike(a)) observers.push(a); } else if (recv.text === "Date" && member === "parse") { - if (node.arguments[0] !== undefined) dates.push({ call: node, arg: node.arguments[0] }); + if (node.arguments.nodes[0] !== undefined) + dates.push({ call: node, arg: node.arguments.nodes[0] }); } } } @@ -106,9 +108,9 @@ export function scanDivergences( if ( ts.isIdentifier(node.expression) && node.expression.text === "Date" && - node.arguments?.length === 1 + node.arguments?.nodes.length === 1 ) { - dates.push({ call: node, arg: node.arguments[0]! }); + dates.push({ call: node, arg: node.arguments.nodes[0]! }); } } else if (ts.isForInStatement(node)) { observers.push(node.expression); @@ -127,12 +129,12 @@ export function scanDivergences( if ( element !== undefined && ts.isArrayLiteralExpression(node.initializer) && - node.initializer.elements.some(isExistingObjectExpression) + node.initializer.elements.nodes.some(isExistingObjectExpression) ) arrayDecls.push({ decl: node, element }); } } else if (ts.isObjectLiteralExpression(node)) { - if (node.properties.length >= 1 && node.properties.every(isPlainProperty)) + if (node.properties.nodes.length >= 1 && node.properties.nodes.every(isPlainProperty)) literals.push(node); } else if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) { casts.push(node); @@ -162,7 +164,7 @@ export function scanDivergences( const identityUsesOf = (fn: ts.FunctionLikeDeclaration): (IdentityUse | null)[] => { let uses = usesByFn.get(fn); if (uses === undefined) { - uses = fn.parameters.map((p) => + uses = fn.parameters.nodes.map((p) => ts.isIdentifier(p.name) && fn.body !== undefined ? identityUse(fn.body, p.name.text) : null, ); usesByFn.set(fn, uses); @@ -181,9 +183,9 @@ export function scanDivergences( if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue; const fn = decl as ts.FunctionLikeDeclaration; const uses = identityUsesOf(fn); - call.arguments.forEach((arg, i) => { + call.arguments.nodes.forEach((arg, i) => { const use = uses[i]; - const param = fn.parameters[i]; + const param = fn.parameters.nodes[i]; if (use === null || use === undefined || param === undefined || param.type === undefined) return; if (!isExistingObjectExpression(arg)) return; @@ -259,7 +261,7 @@ export function scanDivergences( if (scope === undefined) continue; const use = elementWrite(scope, name, decl.end); if (use === null) continue; - for (const item of (decl.initializer as ts.ArrayLiteralExpression).elements) { + for (const item of (decl.initializer as ts.ArrayLiteralExpression).elements.nodes) { if (!isExistingObjectExpression(item)) continue; const copy = safely(() => widthCopyBetween(lowerer, item, element)); if (copy === undefined || copy === null) continue; @@ -303,7 +305,7 @@ export function scanDivergences( if (namedObjectDeclaration(checker, contextual) === null) continue; const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name)); if (declared === undefined) continue; - const written = lit.properties.map((p) => propertyNameText(p)!); + const written = lit.properties.nodes.map((p) => propertyNameText(p)!); const assigned = binding === null ? new Set() : (assignedMembers.get(binding) ?? new Set()); const finding = keyOrderFinding(lowerer, contextual, declared, written, assigned); @@ -324,7 +326,7 @@ export function scanDivergences( // ── one-argument localeCompare over text outside the collation table ─ for (const call of localeCompares) { const access = call.expression as ts.PropertyAccessExpression; - for (const operand of [access.expression, call.arguments[0]!]) { + for (const operand of [access.expression, call.arguments.nodes[0]!]) { const text = stringLiteralText(operand); if (text !== null && !isCollationCovered(text)) { out.push(localeCompareDivergenceDiag(text, locOf(operand))); @@ -348,7 +350,7 @@ export function scanDivergences( let fn: ts.Node | undefined = cast.parent; while (fn !== undefined && !ts.isFunctionLike(fn)) fn = fn.parent; if (fn === undefined || !ts.isFunctionLike(fn)) continue; - const index = fn.parameters.findIndex( + const index = fn.parameters.nodes.findIndex( (p) => ts.isIdentifier(p.name) && p.name.text === inner.text, ); if (index < 0) continue; @@ -376,7 +378,7 @@ export function scanDivergences( const sites = decl === undefined ? undefined : downcasts.get(decl); if (sites === undefined) continue; for (const site of sites) { - const arg = call.arguments[site.index]; + const arg = call.arguments.nodes[site.index]; if (arg === undefined) continue; const argType = safely(() => checker.getTypeAtLocation(arg)); const source = argType === undefined ? null : classDeclarationOfType(checker, argType); @@ -443,7 +445,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp iterated = init.initializer; } if (!ts.isArrayLiteralExpression(iterated)) return []; - return iterated.elements.filter((e) => stringLiteralText(e) !== null); + return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null); } /** A checker query that fails here only loses a warning. */ @@ -529,7 +531,7 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null if ( node !== root && ts.isFunctionLike(node) && - node.parameters.some((p) => ts.isIdentifier(p.name) && p.name.text === name) + node.parameters.nodes.some((p) => ts.isIdentifier(p.name) && p.name.text === name) ) return "skip"; if (ts.isBinaryExpression(node)) { @@ -611,9 +613,9 @@ function arrayElementTypeNode(node: ts.TypeNode): ts.TypeNode | undefined { ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && (node.typeName.text === "Array" || node.typeName.text === "ReadonlyArray") && - node.typeArguments?.length === 1 + node.typeArguments?.nodes.length === 1 ) - return node.typeArguments[0]; + return node.typeArguments.nodes[0]; return undefined; } @@ -657,7 +659,7 @@ function elementWrite(root: ts.Node, name: string, from: number): string | null node.expression.text === name && ts.isVariableDeclarationList(node.initializer) ) { - const binding = node.initializer.declarations[0]?.name; + const binding = node.initializer.declarations.nodes[0]?.name; if (binding !== undefined && ts.isIdentifier(binding)) { const use = identityUse(node.statement, binding.text); if (use?.kind === "write") { @@ -837,8 +839,10 @@ function classExtends( let cur: ts.ClassLikeDeclaration | null = sub; for (let guard = 0; cur !== null && guard < 64; guard++) { if (cur === base) return true; - const heritage = cur.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword); - const expr = heritage?.types[0]?.expression; + const heritage = cur.heritageClauses?.nodes.find( + (h) => h.token === ts.SyntaxKind.ExtendsKeyword, + ); + const expr = heritage?.types?.nodes[0]?.expression; if (expr === undefined) return false; const symbol = lowerer.checker.getSymbolAtLocation(expr); if (symbol === undefined) return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index 2d09efa1a..e3b9bf025 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -21,12 +21,12 @@ export function isNativeProxyInitializer( export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr); const handler = args[1]!; if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) { - for (const prop of handler.properties) { + for (const prop of handler.properties.nodes) { const callback = ts.isMethodDeclaration(prop) ? prop : ts.isPropertyAssignment(prop) && @@ -39,7 +39,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx // ProxyHandler types include symbols, which this boundary // explicitly refuses; they must not force a copying record // parameter or an unboxable string|symbol closure signature. - for (const param of callback.parameters) { + for (const param of callback.parameters.nodes) { if (ts.isIdentifier(param.name) && !param.type && !param.dotDotDotToken) lowerer.checkedCallbackParams.add(param); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 78d783a11..216069362 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -71,7 +71,7 @@ export function foldedStringKeyOf(lowerer: Lowerer, expr: ts.Expression): string // fallback (a template the checker DID type as one literal). let out = expr.head.text; let folded = true; - for (const span of expr.templateSpans) { + for (const span of expr.templateSpans.nodes) { const part = foldedStringKeyOf(lowerer, span.expression); if (part === null) { folded = false; @@ -167,7 +167,7 @@ function pureKeyExpr(lowerer: Lowerer, expr: ts.Expression): boolean { ); } if (ts.isTemplateExpression(expr)) - return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression)); + return expr.templateSpans.nodes.every((s) => pureKeyExpr(lowerer, s.expression)); return false; } @@ -246,7 +246,7 @@ export function lowerDynObjectLiteral( : { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc }; fields = []; }; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if ( ts.isPropertyAssignment(prop) && !ts.isComputedPropertyName(prop.name) && @@ -511,7 +511,7 @@ function fenceClosureProbe( const fieldType: IrType & { kind: "func" } = slotType !== undefined && slotType.kind === "func" ? slotType - : { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN }; + : { kind: "func", params: inner.parameters.nodes.map(() => DYN), ret: DYN }; if (slotType !== undefined && slotType.kind !== "func") return null; const diagsBefore = lowerer.diags.length; try { @@ -562,7 +562,7 @@ function literalUnionArmOf( if (!tsType.isUnionType()) return null; const props: { name: string; type: ts.Type }[] = []; let hasSpread = false; - for (const p of expr.properties) { + for (const p of expr.properties.nodes) { if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { props.push({ name: propNameText(lowerer, p.name), type: lowerer.typeOf(p.initializer) }); } else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) { @@ -698,11 +698,11 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && ((ts.isArrayLiteralExpression(prop.initializer) && - prop.initializer.elements.length === 0) || + prop.initializer.elements.nodes.length === 0) || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))), ) ) { @@ -711,7 +711,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && lowerer.mapTypeOf(lowerer.typeOf(prop.initializer))?.kind === "array" && @@ -726,7 +726,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some((prop) => { + expr.properties.nodes.some((prop) => { let value = ts.isPropertyAssignment(prop) ? prop.initializer : ts.isShorthandPropertyAssignment(prop) @@ -754,8 +754,8 @@ export function lowerObjectLiteral( // literal in checked native storage instead of erasing its prototype ABI. if ( isJsSourceFile(expr.getSourceFile()) && - (expr.properties.length === 0 || - expr.properties.some( + (expr.properties.nodes.length === 0 || + expr.properties.nodes.some( (prop) => ts.isMethodDeclaration(prop) || ts.isGetAccessorDeclaration(prop) || @@ -782,7 +782,7 @@ export function lowerObjectLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.properties.some( + expr.properties.nodes.some( (property) => ts.isSpreadAssignment(property) && (lowerer.builtinNamespaceModuleOf(property.expression)?.startsWith("path") || @@ -802,7 +802,7 @@ export function lowerObjectLiteral( // only arises in checked-dynamic JS. if ( isJsSourceFile(expr.getSourceFile()) && - expr.properties.some((p) => { + expr.properties.nodes.some((p) => { const n = ts.isSpreadAssignment(p) ? undefined : p.name; return n !== undefined && ts.isComputedPropertyName(n); }) @@ -813,7 +813,7 @@ export function lowerObjectLiteral( // message even when they also make the literal's type unmappable // (a getter or a `this`-returning method would otherwise surface as an // opaque type fence on the whole literal). - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { // Supported shapes: FULL spreads of known record shapes read as // identifiers or other side-effect-free reads, all BEFORE any @@ -935,7 +935,7 @@ export function lowerObjectLiteral( // preserves keys, evaluation order, and fields overwritten afterwards. if ( mapped?.kind === "dyn" && - expr.properties.some( + expr.properties.nodes.some( (p) => ts.isSpreadAssignment(p) && lowerer.mapTypeOf(lowerer.typeOf(p.expression))?.kind === "dyn", ) @@ -954,7 +954,7 @@ export function lowerObjectLiteral( // object): `{}` carries no shape information, so the literal builds as // its OWN type, exactly the unmappable-context fallback below. An // empty LITERAL keeps the context (the shapes agree). - if (mapped?.kind === "record" && expr.properties.length > 0) { + if (mapped?.kind === "record" && expr.properties.nodes.length > 0) { const ctxShape = lowerer.shapes.get(mapped.shapeId); if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) { mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped; @@ -970,7 +970,7 @@ export function lowerObjectLiteral( // type carries but the shape dropped (settled value/reason, // generic-callable members) belongs to the drop paths below, and // index-signature contexts keep every key (overflow capture). - if (mapped?.kind === "record" && expr.properties.length > 0) { + if (mapped?.kind === "record" && expr.properties.nodes.length > 0) { const ctxShape = lowerer.shapes.get(mapped.shapeId); if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) { const names = new Set(ctxShape.fields.map((f) => f.name)); @@ -979,7 +979,7 @@ export function lowerObjectLiteral( return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined); }; const extraOf = (text: string): boolean => !names.has(text) && !ctxHasProp(text); - const extra = expr.properties.some((p) => { + const extra = expr.properties.nodes.some((p) => { if (ts.isSpreadAssignment(p) || !p.name) return false; if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) return extraOf(p.name.text); if (ts.isNumericLiteral(p.name)) return extraOf(String(Number(p.name.text))); @@ -1036,7 +1036,7 @@ export function lowerObjectLiteral( !armShape?.tuple && // `{} | undefined` supplies no layout for a populated options // literal, just as a bare empty-record context supplies none. - (expr.properties.length === 0 || + (expr.properties.nodes.length === 0 || (armShape && (armShape.fields.length > 0 || armShape.indexValue))) ) { mapped = recordArms[0]!; @@ -1062,7 +1062,7 @@ export function lowerObjectLiteral( if ( ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId) || - expr.properties.some(ts.isSpreadAssignment) + expr.properties.nodes.some(ts.isSpreadAssignment) ) { const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms); if (arm) mapped = arm; @@ -1080,13 +1080,13 @@ export function lowerObjectLiteral( if ( isJsSourceFile(expr.getSourceFile()) && (!mapped || - expr.properties.some( + expr.properties.nodes.some( (p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p), )) && isCjsExportTableLiteral(expr) ) { const fields: { name: string; value: IrExpr }[] = []; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) continue; const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name; if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName))) continue; @@ -1135,7 +1135,9 @@ export function lowerObjectLiteral( if ( expected && !isJsSourceFile(expr.getSourceFile()) && - expr.properties.every((p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) + expr.properties.nodes.every( + (p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p), + ) ) { const own = lowerer.shapes.get(type.shapeId)!; const target = lowerer.shapes.get(expected.shapeId)!; @@ -1159,7 +1161,7 @@ export function lowerObjectLiteral( // A Date's toJSON() result keeps its null in a field with no declared // destination: the lib types say string. if (!expected) { - for (const property of expr.properties) { + for (const property of expr.properties.nodes) { const value = ts.isPropertyAssignment(property) ? property.initializer : ts.isShorthandPropertyAssignment(property) @@ -1196,7 +1198,7 @@ export function lowerObjectLiteral( // accessor names — the documented divergence). if (isJsSourceFile(expr.getSourceFile())) { const accessorNames = new Set( - expr.properties + expr.properties.nodes .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) .map((p) => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : "", @@ -1224,14 +1226,14 @@ export function lowerObjectLiteral( expr.parent.initializer === expr && ts.isVariableStatement(expr.parent?.parent?.parent) && ts.isSourceFile(expr.parent?.parent?.parent?.parent); - const assignedProperties = expr.properties.filter( + const assignedProperties = expr.properties.nodes.filter( (prop): prop is ts.PropertyAssignment | ts.ShorthandPropertyAssignment => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop), ); if ( topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) && - assignedProperties.length === expr.properties.length + assignedProperties.length === expr.properties.nodes.length ) { const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name))); if (shape.fields.some((field) => !provided.has(field.name))) { @@ -1261,11 +1263,11 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && !shapeHasAccessorSlots(shape) && - expr.properties.length >= 2 && - ts.isSpreadAssignment(expr.properties[0]!) && - !conditionalSpreadOf(expr.properties[0]!.expression) && - !andSpreadOf(lowerer, expr.properties[0]!.expression) && - expr.properties + expr.properties.nodes.length >= 2 && + ts.isSpreadAssignment(expr.properties.nodes[0]!) && + !conditionalSpreadOf(expr.properties.nodes[0]!.expression) && + !andSpreadOf(lowerer, expr.properties.nodes[0]!.expression) && + expr.properties.nodes .slice(1) .every( (p) => @@ -1277,8 +1279,8 @@ export function lowerObjectLiteral( (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)), ) ) { - const spread = expr.properties[0]! as ts.SpreadAssignment; - const props = expr.properties.slice(1) as ( + const spread = expr.properties.nodes[0]! as ts.SpreadAssignment; + const props = expr.properties.nodes.slice(1) as ( | ts.PropertyAssignment | ts.ShorthandPropertyAssignment )[]; @@ -1334,12 +1336,12 @@ export function lowerObjectLiteral( shape.indexValue && shape.fields.length === 0 && !shape.tuple && - (expr.properties.some((p) => ts.isSpreadAssignment(p)) || - expr.properties.some(isRuntimeComputedKey)) + (expr.properties.nodes.some((p) => ts.isSpreadAssignment(p)) || + expr.properties.nodes.some(isRuntimeComputedKey)) ) { const contributors: IndexMergeContributor[] = []; let mergeable = true; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { @@ -1463,7 +1465,7 @@ export function lowerObjectLiteral( } // Unfoldable computed keys outside the index-signature merge path: // no record shape can hold a runtime-decided name. - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (isRuntimeComputedKey(prop)) { lowerer.unsupported( "SC1090", @@ -1481,14 +1483,14 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && shape.fields.length > 0 && - expr.properties.length > 0 && - expr.properties.every( + expr.properties.nodes.length > 0 && + expr.properties.nodes.every( (p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression) && !andSpreadOf(lowerer, p.expression), ) && - expr.properties.some((p) => { + expr.properties.nodes.some((p) => { const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression)); return ( t?.kind === "dyn" || (t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue) @@ -1515,7 +1517,7 @@ export function lowerObjectLiteral( const fields: { name: string; value: IrExpr; overflow?: true }[] = []; const spreadStmts: IrStmt[] = []; - const hasSpread = expr.properties.some(ts.isSpreadAssignment); + const hasSpread = expr.properties.nodes.some(ts.isSpreadAssignment); const captured = new Set(); const capture = (value: IrExpr): IrExpr => { const local = lowerer.declareHiddenLocal("%spreadValue", value.type); @@ -1594,7 +1596,7 @@ export function lowerObjectLiteral( // Member names DROPPED from the shape (JS deferral — unlowerable pure // member reads narrow away; see the catch in the property loop). const droppedNames = new Set(); - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { // A later contributor can mutate an earlier source or overwrite a // value with effects. Snapshot pending fields at their source position, // before evaluating the next contributor, even if they are overwritten. @@ -1672,7 +1674,7 @@ export function lowerObjectLiteral( let srcNode: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression; const srcLowered = capture( - prop === expr.properties[0] && leadingSpreadLowered !== null + prop === expr.properties.nodes[0] && leadingSpreadLowered !== null ? leadingSpreadLowered : lowerer.lowerExpr(srcNode), ); @@ -1732,7 +1734,7 @@ export function lowerObjectLiteral( ); } const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -1968,7 +1970,7 @@ export function lowerObjectLiteral( // exactly like Node does. Conditional spreads don't count (their // empty arm defines nothing) — their own collision fences hold. const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -2214,7 +2216,7 @@ export function lowerObjectLiteral( // reads; the field promotion below adopts that layout. (fieldType?.kind === "record" && ts.isObjectLiteralExpression(init) && - init.properties.length > 0 + init.properties.nodes.length > 0 ? (() => { const nested = lowerer.lowerObjectLiteral(init, fieldType); return lowerer.isRuntimeOptionalRecordVariant(nested.type, fieldType) @@ -2347,7 +2349,7 @@ export function lowerObjectLiteral( * fields needs a different result layout and stays on the ordinary path. */ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null { if (isJsSourceFile(expr.getSourceFile())) return null; - const spread = expr.properties[0]; + const spread = expr.properties.nodes[0]; if (!spread || !ts.isSpreadAssignment(spread)) return null; let sourceNode = spread.expression; while (ts.isParenthesizedExpression(sourceNode) || ts.isSatisfiesExpression(sourceNode)) @@ -2361,7 +2363,7 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "record")) return null; const props: (ts.PropertyAssignment | ts.ShorthandPropertyAssignment)[] = []; const names: string[] = []; - for (const prop of expr.properties.slice(1)) { + for (const prop of expr.properties.nodes.slice(1)) { if ( (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) || (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) @@ -2557,9 +2559,9 @@ export function andSpreadOf( ) ) return null; - if (right.properties.length === 0) return "unsupported"; + if (right.properties.nodes.length === 0) return "unsupported"; const props: CondSpreadProp[] = []; - for (const p of right.properties) { + for (const p of right.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2595,12 +2597,14 @@ export function conditionalSpreadOf( if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) { return "unsupported"; } - const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0; - const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0; + const trueCarries = + whenFalse.properties.nodes.length === 0 && whenTrue.properties.nodes.length > 0; + const falseCarries = + whenTrue.properties.nodes.length === 0 && whenFalse.properties.nodes.length > 0; if (!trueCarries && !falseCarries) return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar const carrier = trueCarries ? whenTrue : whenFalse; const props: CondSpreadProp[] = []; - for (const p of carrier.properties) { + for (const p of carrier.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2643,7 +2647,7 @@ function lowerDeclaredSpreadMerge( shape?: IrRecordShape; } const srcs: Src[] = []; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { const spread = prop as ts.SpreadAssignment; // caller-checked: all spreads const value = lowerer.lowerExpr(spread.expression); if (value.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 55feafc62..930e1a7cb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -253,7 +253,7 @@ export function lowerOptionalChain( } } const spreads: { arg: number; what: string }[] = []; - const args = expr.arguments.map((arg, index) => { + const args = expr.arguments.nodes.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); @@ -429,7 +429,7 @@ export function lowerOptionalChain( if (dotNode === expr && ts.isCallExpression(expr)) { if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode)); const params = narrowed.params; - const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc }; return lowerer.finishOptionalChain(expr, id, receiver, body, loc); } diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index a3cf5c0ce..f413f4680 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -148,7 +148,7 @@ export function declaredReturnType( let mappedReturn = lowerer.mapTypeOf(retTsType); if ( isJsSourceFile(decl.getSourceFile()) && - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !decl.asteriskToken && mappedReturn?.kind !== "promise" ) { @@ -240,8 +240,8 @@ export function declaredReturnType( !hasExplicitJsDocReturn(decl) && mappedReturn !== null && promiseCarriesDyn(lowerer, mappedReturn) && - !decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && - decl.parameters.some((param) => { + !decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.parameters.nodes.some((param) => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(param.name)); return mapped === null || mapped.kind === "dyn"; }) @@ -278,7 +278,7 @@ export function declaredReturnType( // Native-only members (such as Int32Array layout offsets) cannot cross // that boundary; their factories keep the native record ABI. const asyncReturn = - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && mappedReturn?.kind === "promise"; const recordReturn = asyncReturn && mappedReturn?.kind === "promise" ? mappedReturn.inner : mappedReturn; @@ -303,7 +303,7 @@ export function declaredReturnType( recordReturn !== undefined && inferredRecordReturn && lowerer.dynConvertible(recordReturn) && - decl.parameters.every((p) => { + decl.parameters.nodes.every((p) => { const mt = lowerer.mapTypeOf(lowerer.typeOf(p)); return mt === null || mt.kind === "dyn"; }) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 67dfb2408..bf5f2563d 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -207,11 +207,11 @@ function requireStableGenericBinding( export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDeclaration): void { const typeParams = declaredTypeParameterSymbols( lowerer, - decl.typeParameters!, + decl.typeParameters!.nodes, decl, "this function form", ); - checkGenericParameterNames(lowerer, decl.parameters, true); + checkGenericParameterNames(lowerer, decl.parameters.nodes, true); const nameText = decl.name?.text ?? "%default"; // nameless = the default export (checked by collectSignatureInner) const symbol = declSymbolOf(lowerer, decl); if (!symbol) lowerer.unsupported("SC1090", decl, "this function form"); @@ -245,7 +245,7 @@ export function lowerGenericCall( const instance = info.implicitParams ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); - const args = lowerer.completeArgs(expr.arguments, instance.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } @@ -279,7 +279,7 @@ export function genericCallInstance( ); } } - if (!rsig || rsig.getParameters().length !== info.decl.parameters.length) { + if (!rsig || rsig.getParameters().length !== info.decl.parameters.nodes.length) { lowerer.unsupported("SC1090", expr, "this call form"); } // Per-param shapes from the RESOLVED signature (types substituted) plus @@ -288,9 +288,9 @@ export function genericCallInstance( // paramShape rules, applied to post-substitution types. const params = paramShapes( lowerer, - info.decl.parameters, + info.decl.parameters.nodes, rsig, - (_param, i) => expr.arguments[i] ?? expr, + (_param, i) => expr.arguments.nodes[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); @@ -438,7 +438,7 @@ export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrTyp /** Whether `node` is declared inside a function with type parameters. */ function insideGenericFunction(node: ts.Node): boolean { for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true; + if (ts.isFunctionLike(parent) && (parent.typeParameters?.nodes.length ?? 0) > 0) return true; return false; } @@ -556,7 +556,7 @@ function internGenericInstance( function keyofConstrainedTypeParams(info: GenericFnInfo): Set { if (info.keyofTps) return info.keyofTps; const out = new Set(); - info.decl.typeParameters?.forEach((tpDecl, i) => { + info.decl.typeParameters?.nodes.forEach((tpDecl, i) => { const sym = info.typeParams[i]; if (!sym || tpDecl.constraint === undefined) return; if ( @@ -586,7 +586,7 @@ export function inferTypeParamBindings( tsBindings?: Map, ): Map { const bindings = new Map(); - expr.typeArguments?.forEach((ta, i) => { + expr.typeArguments?.nodes.forEach((ta, i) => { const tp = info.typeParams[i]; if (!tp) return; const taT = lowerer.checker.getTypeFromTypeNode(ta); @@ -597,7 +597,13 @@ export function inferTypeParamBindings( } }); unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings, tsBindings); + bindDefaultTypeParams( + lowerer, + info.typeParams, + info.decl.typeParameters?.nodes, + bindings, + tsBindings, + ); return bindings; } @@ -847,7 +853,7 @@ export function lowerGenericInstance( // body returns the resolved promise's INNER type, calls enter through // the instance's own spawn wrapper (the emitter routes by fn.async), // and awaits park this instance's fibers. - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const nameBlame: ts.Node = (ts.isArrowFunction(decl) ? undefined : decl.name) ?? decl; const isGenerator = decl.asteriskToken !== undefined; if (isAsync && !isGenerator && inst.returnType.kind !== "promise") { @@ -909,12 +915,12 @@ export function lowerGenericInstance( } // Default-param initializers lower per instance, with the bindings // threaded — a default mentioning T resolves like any body expression. - const declared = lowerer.declareParams(decl.parameters, inst.params); + const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); params.push(...declared.params); const body = [...receiverInit, ...declared.prologue]; const bodyBlock = blockBodyOf(decl); if (bodyBlock) { - body.push(...lowerer.lowerStmts(bodyBlock.statements)); + body.push(...lowerer.lowerStmts(bodyBlock.statements.nodes)); if (fnCtx.inferReturn) { bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl); inst.returnType = bodyReturn; @@ -1053,7 +1059,7 @@ export function lowerGenericFnValue( unifySignatureBindings(lowerer, info, target, bindings); const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info); if (hofBinding) bindings.set(hofBinding.typeParam, hofBinding.type); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings); + bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters?.nodes, bindings); // A pinning signature that itself keeps type parameters (`let g: (x: // T) => T = identity` — storing the generic signature as such) binds // nothing: mapType answers null for an unsubstituted parameter. @@ -1076,13 +1082,13 @@ function runtimeOptionalHofGenericBinding( while (argument.parent && ts.isParenthesizedExpression(argument.parent)) argument = argument.parent; const call = argument.parent; - if (!ts.isCallExpression(call) || call.arguments[0] !== argument) return null; + if (!ts.isCallExpression(call) || call.arguments.nodes[0] !== argument) return null; if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "map") return null; if (!lowerer.isStdlibMember(call.expression)) return null; const receiver = lowerer.mapTypeOf(lowerer.typeOf(call.expression.expression)); if (receiver?.kind !== "array") return null; - const parameter = info.decl.parameters[0]; + const parameter = info.decl.parameters.nodes[0]; if (!parameter || !ts.isIdentifier(parameter.name)) return null; const parameterType = lowerer.typeOf(parameter.name); if ((parameterType.flags & ts.TypeFlags.TypeParameter) === 0) return null; @@ -1138,7 +1144,7 @@ function genericValueInstance( try { const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl); if (!declSig) lowerer.unsupported("SC1090", ref, "this function form"); - const params = paramShapes(lowerer, info.decl.parameters, declSig); + const params = paramShapes(lowerer, info.decl.parameters.nodes, declSig); const retTs = lowerer.checker.getReturnTypeOfSignature(declSig); const returnType = lowerer.mapTypeOf(retTs); if (!returnType) { @@ -1300,11 +1306,11 @@ export function implicitPatternsOf( lowerer: Lowerer, decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, ): (ImplicitPattern | null)[] { - return decl.parameters.map((param): ImplicitPattern | null => { + return decl.parameters.nodes.map((param): ImplicitPattern | null => { if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; const elements: ImplicitPattern["elements"] = []; - for (const element of param.name.elements) { + for (const element of param.name.elements.nodes) { if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) return null; const key = element.propertyName ?? element.name; @@ -1341,7 +1347,7 @@ function implicitPatternBinding( const values = new Map(); /** Shorthand properties name the binding they read. */ const shorthand = new Map(); - for (const property of literal.properties) { + for (const property of literal.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) { values.set(property.name.text, property.name); const read = lowerer.checker.getShorthandAssignmentValueSymbol(property); @@ -1407,13 +1413,13 @@ export function implicitAnyParamSymbolsOf( ): (ts.Symbol | null)[] | null { if (!decl.body) return null; if (decl.asteriskToken) return null; - if (decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; if (decl.typeParameters !== undefined) return null; // real generics own the machinery - if (decl.parameters.length === 0) return null; + if (decl.parameters.nodes.length === 0) return null; // The variadic-`arguments` form keeps its dynRest story whole. if (bodyReadsArguments(decl)) return null; let hasImplicitParams = false; - const out = decl.parameters.map((param): ts.Symbol | null => { + const out = decl.parameters.nodes.map((param): ts.Symbol | null => { if (!ts.isIdentifier(param.name)) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; if (param.name.text === "this") return null; @@ -1456,7 +1462,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // Infer their result from the specialized body as well. if ( info.member?.kind === "static" && - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "object", @@ -1464,7 +1470,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | ) return null; const broadCallbackSymbols = new Set( - info.decl.parameters.flatMap((param, index) => { + info.decl.parameters.nodes.flatMap((param, index) => { const symbol = info.implicitParams?.[index]; return symbol !== null && symbol !== undefined && @@ -1507,7 +1513,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // container instead of checking every element against that guess. if (!hasExplicitJsDocReturn(info.decl)) return null; if ( - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array", @@ -1661,8 +1667,8 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if ( ts.isObjectLiteralExpression(arg) && isJsSourceFile(arg.getSourceFile()) && - (arg.properties.length === 0 || - arg.properties.some( + (arg.properties.nodes.length === 0 || + arg.properties.nodes.some( (property) => ts.isMethodDeclaration(property) || ts.isAccessor(property) || @@ -1726,9 +1732,9 @@ export function implicitCallInstance( /** The destructured names' bound types, which the object's own * representation does not always carry. */ const patternKey: string[] = []; - info.decl.parameters.forEach((param, i) => { + info.decl.parameters.nodes.forEach((param, i) => { const pattern = patterns[i]; - const patternArg = call.arguments[i]; + const patternArg = call.arguments.nodes[i]; if (pattern && patternArg && !ts.isSpreadElement(patternArg)) { // The argument's object type binds each destructured name, so a // class passed as `{ Base }` keeps its static class type. @@ -1751,7 +1757,7 @@ export function implicitCallInstance( let bound: IrType = DYN; const declared = lowerer.paramShape(param); if ((lowerer.typeOf(param.name).flags & ts.TypeFlags.Any) === 0) bound = declared.type; - const arg = call.arguments[i]; + const arg = call.arguments.nodes[i]; if (arg && !ts.isSpreadElement(arg)) { const stored = storedImplicitArgumentType(lowerer, arg); if (stored?.kind === "dyn") { @@ -1891,15 +1897,15 @@ export function implicitDefaultInstance( blame: ts.Node, info: GenericFnInfo, ): GenericInstance { - const shapes: ParamShape[] = info.decl.parameters.map((param, i) => + const shapes: ParamShape[] = info.decl.parameters.nodes.map((param, i) => info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param), ); const argTypes = new Map(); // Newly bindable nominal class parameters must not retain their JSDoc // receiver type in the checked fallback. Keep existing broad JS inference. - for (let i = 0; i < info.decl.parameters.length; i++) { + for (let i = 0; i < info.decl.parameters.nodes.length; i++) { const symbol = info.implicitParams?.[i]; - const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind; + const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters.nodes[i]!.name))?.kind; if (symbol && (kind === "object" || kind === "array")) { argTypes.set(symbol, lowerer.checker.getUnknownType()); } @@ -1935,7 +1941,7 @@ function internImplicitInstance( ); } const rendered = shapes.map((s) => lowerer.fmt(s.type)).join(", "); - const dynamicBroadPromise = info.decl.parameters.some( + const dynamicBroadPromise = info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] !== null && info.implicitParams?.[index] !== undefined && @@ -1976,7 +1982,9 @@ function internImplicitInstance( // this exact committed instance body here; repeated instantiations of // the same declaration find the facade memos warm. lowerer.checker.prefetchRoots([ - ...info.decl.parameters.flatMap((param) => (param.initializer ? [param.initializer] : [])), + ...info.decl.parameters.nodes.flatMap((param) => + param.initializer ? [param.initializer] : [], + ), ...(info.decl.body ? [info.decl.body] : []), ]); const fn = lowerer.lowerGenericInstance(info, inst); @@ -2179,7 +2187,7 @@ export function objLitGenericFnInfoOf( const existing = lowerer.objLitGenericFns.get(fnNode); if (existing) return existing; if (fnNode.asteriskToken) lowerer.unsupported("SC1071", blame); - if (fnNode.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (fnNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", blame, "async object-literal generic methods"); } // `this` is the receiver object — records don't model it (the @@ -2198,11 +2206,11 @@ export function objLitGenericFnInfoOf( } const typeParams = declaredTypeParameterSymbols( lowerer, - fnNode.typeParameters!, + fnNode.typeParameters!.nodes, blame, "this method form", ); - checkGenericParameterNames(lowerer, fnNode.parameters, false); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, false); const info: GenericFnInfo = { decl: fnNode, baseName: name, @@ -2312,7 +2320,12 @@ export function bindingGenericFnInfoOf( const typeParams: ts.Symbol[] = []; if (fnNode.typeParameters !== undefined) { typeParams.push( - ...declaredTypeParameterSymbols(lowerer, fnNode.typeParameters, fnNode, "this function form"), + ...declaredTypeParameterSymbols( + lowerer, + fnNode.typeParameters.nodes, + fnNode, + "this function form", + ), ); } else { // The CONTEXTUAL shape (bindingContextualGenericFnNodeOf): the type @@ -2328,7 +2341,7 @@ export function bindingGenericFnInfoOf( typeParams.push(tpSym); } } - checkGenericParameterNames(lowerer, fnNode.parameters, true); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, true); const stmt = decl.parent?.parent; // declarator → list → statement (nsPathPrefix wants the statement) const info: GenericFnInfo = { decl: fnNode, diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts index 6d011b5ee..baf550e6d 100644 --- a/packages/compiler/src/frontend/lowering/import-meta.ts +++ b/packages/compiler/src/frontend/lowering/import-meta.ts @@ -65,7 +65,7 @@ export function importMetaBindingSource( ): IrExpr | null { if (!isImportMeta(initializer) || !ts.isObjectBindingPattern(pattern)) return null; const fields: { name: string; value: IrExpr }[] = []; - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.dotDotDotToken) lowerer.unsupported( "SC1031", diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index 522563355..32bb71858 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -168,7 +168,7 @@ function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes export function lowerInternTableNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!ts.isIdentifier(expr.expression)) return null; if (experimentalImportOf(lowerer.program, expr.expression) !== "InternTable") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const arg = args[0]; if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) lowerer.noLowering(`new InternTable with ${args.length} arguments`, expr); @@ -265,11 +265,14 @@ export function lowerInternTableCall(lowerer: Lowerer, expr: ts.CallExpression): if (expr.questionDotToken !== undefined) lowerer.noLowering(`InternTable.${member}?.() calls`, expr, `call table.${member}() directly`); const arity = member === "get" || member === "has" ? 1 : 2; - if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`InternTable.${member} with ${expr.arguments.length} arguments`, expr); + if ( + expr.arguments.nodes.length !== arity || + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) + lowerer.noLowering(`InternTable.${member} with ${expr.arguments.nodes.length} arguments`, expr); const types = tableTypes(lowerer, lowerer.typeOf(callee.expression), expr); const table = receiverOf(lowerer, callee); - const keyNode = expr.arguments[0]!; + const keyNode = expr.arguments.nodes[0]!; const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), types.key); if (member === "has") return { kind: "toBool", operand: find(handleOf(table, loc), key, loc), type: BOOL, loc }; @@ -285,7 +288,7 @@ export function lowerInternTableCall(lowerer: Lowerer, expr: ts.CallExpression): loc, }; } - const valueNode = expr.arguments[1]!; + const valueNode = expr.arguments.nodes[1]!; const expected = member === "getOrInsert" ? types.value : funcOf([types.key], types.value); if ( member === "getOrInsertComputed" && @@ -399,10 +402,10 @@ function inlineArrowBody( value: IrType, ): IrExpr | null { if (!ts.isArrowFunction(node) || ts.isBlock(node.body)) return null; - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - const param = node.parameters[0]; + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + const param = node.parameters.nodes[0]; if ( - node.parameters.length > 1 || + node.parameters.nodes.length > 1 || (param !== undefined && (!ts.isIdentifier(param.name) || param.initializer !== undefined || diff --git a/packages/compiler/src/frontend/lowering/js-class-fields.ts b/packages/compiler/src/frontend/lowering/js-class-fields.ts index 4df513c91..2f726ce97 100644 --- a/packages/compiler/src/frontend/lowering/js-class-fields.ts +++ b/packages/compiler/src/frontend/lowering/js-class-fields.ts @@ -10,7 +10,7 @@ const PROPERTY_REFLECTION = new Set(["deleteProperty", "freeze", "seal", "preven function freshLiteralArgument(callee: ts.PropertyAccessExpression): boolean { const call = callee.parent; if (!ts.isCallExpression(call) || call.expression !== callee) return false; - let argument = call.arguments[0]; + let argument = call.arguments.nodes[0]; while (argument && ts.isParenthesizedExpression(argument)) argument = argument.expression; return ( argument !== undefined && @@ -101,12 +101,12 @@ export function constructionPrefix( ): Construction | null { const created = new Set(base?.created ?? []); let sealed = base?.sealed ?? true; - const ctor = decl.members.find( + const ctor = decl.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); - if (ctor?.parameters.some((parameter) => usesReceiver(parameter))) sealed = false; - const statements = ctor?.body?.statements ?? []; + if (ctor?.parameters?.nodes.some((parameter) => usesReceiver(parameter))) sealed = false; + const statements = ctor?.body?.statements?.nodes ?? []; // A derived constructor's statements before super() cannot reach the // instance; the field declarations run when super() returns. let at = 0; @@ -119,12 +119,12 @@ export function constructionPrefix( ); if (superAt < 0) return null; const call = (statements[superAt] as ts.ExpressionStatement).expression as ts.CallExpression; - if (call.arguments.some(usesReceiver)) sealed = false; + if (call.arguments.nodes.some(usesReceiver)) sealed = false; at = superAt + 1; } - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) continue; - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + if (member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) continue; if (member.initializer && usesReceiver(member.initializer)) sealed = false; if (sealed) created.add(member.name.text); diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index 1d7ab6eee..8f17a2448 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -57,10 +57,11 @@ function isAssignmentOperator(kind: ts.SyntaxKind): boolean { * no heritage, decorators or other members, a constructor body that * returns a value at its own level and never mentions `this`/`super`. */ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boolean { - if (decl.heritageClauses !== undefined && decl.heritageClauses.length > 0) return false; - if (decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) return false; + if (decl.heritageClauses !== undefined && decl.heritageClauses.nodes.length > 0) return false; + if (decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) + return false; let ctor: ts.ConstructorDeclaration | undefined; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (ts.isSemicolonClassElement(member)) continue; if (!ts.isConstructorDeclaration(member) || ctor !== undefined) return false; ctor = member; @@ -83,7 +84,7 @@ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boo node.forEachChild(visit); }; ctor.body.forEachChild(visit); - for (const parameter of ctor.parameters) parameter.forEachChild(visit); + for (const parameter of ctor.parameters.nodes) parameter.forEachChild(visit); return returnsValue && !observesThis; } @@ -97,7 +98,7 @@ function insideArrow(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean { function markNested(literal: ts.ObjectLiteralExpression, out: Set): void { if (out.has(literal)) return; out.add(literal); - for (const property of literal.properties) { + for (const property of literal.properties.nodes) { if (!ts.isPropertyAssignment(property)) continue; const value = unparenthesized(property.initializer); if (ts.isObjectLiteralExpression(value)) markNested(value, out); @@ -128,7 +129,7 @@ export function jsDynamicDeclarations( const init = unparenthesized(node.initializer); // An empty literal's binding is already checked-dynamic // (jsOpenObjectType), and its type describes no layout. - if (!ts.isObjectLiteralExpression(init) || init.properties.length === 0) return; + if (!ts.isObjectLiteralExpression(init) || init.properties.nodes.length === 0) return; const symbol = checker.getSymbolAtLocation(node.name); if (symbol !== undefined) literals.set(symbol, init); return; @@ -158,7 +159,7 @@ export function jsDynamicDeclarations( callee.expression.text === "Object" && (callee.name.text === "defineProperty" || callee.name.text === "defineProperties") ) - receiver(node.arguments[0]); + receiver(node.arguments.nodes[0]); } }, SCANNED_KINDS, @@ -195,8 +196,8 @@ export function lowerValueReturningConstructorNew( : undefined; if (decl === undefined) return null; const loc = locOf(expr); - const ctor = decl.members.find(ts.isConstructorDeclaration)!; - const simple = ctor.parameters.every( + const ctor = decl.members.nodes.find(ts.isConstructorDeclaration)!; + const simple = ctor.parameters.nodes.every( (p) => ts.isIdentifier(p.name) && p.dotDotDotToken === undefined && p.initializer === undefined, ); if (!simple || !ts.isSourceFile(decl.parent)) { @@ -206,7 +207,7 @@ export function lowerValueReturningConstructorNew( "constructing a class whose constructor returns a value, with parameter defaults, rest or patterns, or declared inside a function", ); } - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { lowerer.unsupported( "SC1090", @@ -223,11 +224,14 @@ export function lowerValueReturningConstructorNew( lowerer.currentClass = null; lowerer.fnStack.push(newFnCtx(false, null, null, DYN)); try { - const shapes: ParamShape[] = ctor.parameters.map(() => ({ type: DYN, mode: "required" })); - const declared = lowerer.declareParams(ctor.parameters, shapes); + const shapes: ParamShape[] = ctor.parameters.nodes.map(() => ({ + type: DYN, + mode: "required", + })); + const declared = lowerer.declareParams(ctor.parameters.nodes, shapes); const body: IrStmt[] = [ ...declared.prologue, - ...lowerer.lowerStmts(ctor.body!.statements), + ...lowerer.lowerStmts(ctor.body!.statements.nodes), { kind: "runtimeFence", code: "SC1090", @@ -248,12 +252,12 @@ export function lowerValueReturningConstructorNew( lowerer.currentClass = savedClass; } } - const lowered = ctor.parameters.map((_, i) => + const lowered = ctor.parameters.nodes.map((_, i) => args[i] !== undefined ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc), ); // Surplus arguments still evaluate, left to right, after the others. const surplus: IrStmt[] = args - .slice(ctor.parameters.length) + .slice(ctor.parameters.nodes.length) .map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) })); const result = lowerer.declareHiddenLocal("%constructed", DYN); const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc }; diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index fc5fab24d..b212ae473 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -77,8 +77,8 @@ export function lifetimeRegionType(lowerer: Lowerer): IrType { export function lowerLifetimeRegionNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!ts.isIdentifier(expr.expression)) return null; if (experimentalImportOf(lowerer.program, expr.expression) !== "LifetimeRegion") return null; - if ((expr.arguments?.length ?? 0) !== 0) - lowerer.noLowering(`new LifetimeRegion with ${expr.arguments!.length} arguments`, expr); + if ((expr.arguments?.nodes.length ?? 0) !== 0) + lowerer.noLowering(`new LifetimeRegion with ${expr.arguments!.nodes.length} arguments`, expr); const type = lifetimeRegionType(lowerer); return { kind: "new", className: LIFETIME_REGION_CLASS, args: [], type, loc: locOf(expr) }; } @@ -102,8 +102,11 @@ export function lowerLifetimeRegionCall(lowerer: Lowerer, expr: ts.CallExpressio ); const region = lowerer.coerceInto(callee.expression, lowerer.lowerExpr(callee.expression), type); if (member === "release") { - if (expr.arguments.length !== 0) - lowerer.noLowering(`LifetimeRegion.release with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 0) + lowerer.noLowering( + `LifetimeRegion.release with ${expr.arguments.nodes.length} arguments`, + expr, + ); return { kind: "intrinsic", name: "region.release", @@ -121,9 +124,9 @@ export function lowerLifetimeRegionCall(lowerer: Lowerer, expr: ts.CallExpressio loc, }; } - const arg = expr.arguments[0]; - if (expr.arguments.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) - lowerer.noLowering(`LifetimeRegion.run with ${expr.arguments.length} arguments`, expr); + const arg = expr.arguments.nodes[0]; + if (expr.arguments.nodes.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) + lowerer.noLowering(`LifetimeRegion.run with ${expr.arguments.nodes.length} arguments`, expr); const resultTs = lowerer.typeOf(expr); const mapped = resultTs.flags & ts.TypeFlags.Void ? VOID : lowerer.mapTypeOf(resultTs); if (mapped === null) lowerer.badType(expr, resultTs); diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 9d434eae1..8c49db594 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -271,11 +271,11 @@ function lowerAssertLooseEqual( ): IrExpr { const surface = negated ? "assert.notEqual" : "assert.equal"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]; - const bNode = expr.arguments[1]; + const aNode = expr.arguments.nodes[0]; + const bNode = expr.arguments.nodes[1]; if (!aNode || !bNode) throw new InternalCompilerError(`${surface} arity fence returned unexpectedly`); const left = lowerer.lowerExpr(aNode); @@ -329,7 +329,7 @@ function lowerAssertLooseEqual( }); const actual = inspectOnFailure(a); const expected = inspectOnFailure(b); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); const result: IrExpr = { kind: "libCall", fn: "assert.looseResult", @@ -352,7 +352,7 @@ function lowerComparedOperands( loc: SrcLoc, ): { a: IrExpr; b: IrExpr } { const emptyArrayLit = (n: ts.Expression): boolean => - ts.isArrayLiteralExpression(n) && n.elements.length === 0; + ts.isArrayLiteralExpression(n) && n.elements.nodes.length === 0; const emptyOf = (t: IrType): IrExpr => ({ kind: "arrayLit", elems: [], type: t, loc }); // A dyn other side adopts number[] — the dyn array a boxed empty // literal builds is elementless, so any elem type serves. @@ -408,13 +408,13 @@ function lowerAssertLooseDeep( ): IrExpr { const surface = negated ? "assert.notDeepEqual" : "assert.deepEqual"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); refuseUnrenderedDeepOperands( lowerer, surface, @@ -532,13 +532,13 @@ function boxDeepOperand( function lowerAssertPartialDeep(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const surface = "assert.partialDeepStrictEqual"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); refuseUnrenderedDeepOperands( lowerer, surface, @@ -888,14 +888,14 @@ export function lowerAssertDirectCall( * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, expr.expression.getText()); - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `${expr.expression.getText()} with ${expr.arguments.length} arguments`, + `${expr.expression.getText()} with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are assert(value) and assert(value, message)", ); } - const valueNode = expr.arguments[0]!; + const valueNode = expr.arguments.nodes[0]!; const stmts: IrStmt[] = []; const value = stableValue(lowerer, stmts, lowerer.lowerExpr(valueNode), "%assertOkValue", loc); const pass = stableValue( @@ -905,7 +905,7 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): "%assertOkPass", loc, ); - const msgNode = expr.arguments[1]; + const msgNode = expr.arguments.nodes[1]; let msg: IrExpr; let generated = false; if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) { @@ -936,16 +936,16 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): * assert.ok over a false literal. */ function lowerAssertFail(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, "assert.fail"); - if (expr.arguments.length > 1) { + if (expr.arguments.nodes.length > 1) { lowerer.noLowering( - `assert.fail with ${expr.arguments.length} arguments`, + `assert.fail with ${expr.arguments.nodes.length} arguments`, expr, "the supported form is fail(message?) — the deprecated fail(actual, expected, ...) has no lowering", ); } let msg: IrExpr; let generated: IrExpr; - const msgNode = expr.arguments[0]; + const msgNode = expr.arguments.nodes[0]; const stmts: IrStmt[] = []; if (!msgNode || (ts.isIdentifier(msgNode) && msgNode.text === "undefined")) { msg = { kind: "strLit", value: "Failed", type: STRING, loc }; @@ -996,13 +996,13 @@ function lowerAssertEqual( ): IrExpr { const surface = `assert.${deep ? (negated ? "notDeepStrictEqual" : "deepStrictEqual") : negated ? "notStrictEqual" : "strictEqual"}`; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const aNode = expr.arguments[0]!; - const bNode = expr.arguments[1]!; + const aNode = expr.arguments.nodes[0]!; + const bNode = expr.arguments.nodes[1]!; const { a, b } = lowerComparedOperands(lowerer, aNode, bNode, loc); - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); // An Assert instance with skipPrototype: Node compares own properties // without the prototype check, so operands of different static types (two // classes, a class and a record) can be equal. They compare as @@ -1155,8 +1155,8 @@ function lowerAssertEqual( // the call through a binding whose DECLARED constructor differs from the // value's runtime one compares by the declared brand (SEMANTICS.md). if (a.type.kind === "bytes" && typeEquals(a.type, b.type)) { - const brandA = bytesBrandOf(lowerer, expr.arguments[0]!); - const brandB = bytesBrandOf(lowerer, expr.arguments[1]!); + const brandA = bytesBrandOf(lowerer, expr.arguments.nodes[0]!); + const brandB = bytesBrandOf(lowerer, expr.arguments.nodes[1]!); if (!deep) { // Brands shape only the failure HEADER (same-structure vs // reference-equal expectation); an unclassifiable side defers to the @@ -1173,7 +1173,7 @@ function lowerAssertEqual( if (!skipPrototype && (brandA === null || brandB === null)) { lowerer.noLowering( `${surface} of typed-array values whose constructor is not in the static type`, - brandA === null ? expr.arguments[0]! : expr.arguments[1]!, + brandA === null ? expr.arguments.nodes[0]! : expr.arguments.nodes[1]!, "Node's deep equality compares prototypes (a Buffer is never deep-equal to a plain Uint8Array) — annotate the value so its constructor is visible", ); } @@ -1350,19 +1350,19 @@ function lowerAssertMatch( ): IrExpr { const surface = negated ? "assert.doesNotMatch" : "assert.match"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 2 || expr.arguments.length > 3) { - lowerer.noLowering(`${surface} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length < 2 || expr.arguments.nodes.length > 3) { + lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr); } - const s = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const re = lowerer.lowerExpr(expr.arguments[1]!); + const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const re = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (re.type.kind !== "regex") { lowerer.noLowering( `${surface} with a non-regex pattern`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "the second argument must be a regular expression", ); } - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); const neg: IrExpr = { kind: "boolLit", value: negated, type: BOOL, loc }; return { kind: "libCall", fn: "assert.match", args: [s, re, neg, msg, hasMsg], type: VOID, loc }; } @@ -1478,7 +1478,7 @@ function classifyThrowsExpected( let obj: ts.Expression = node; while (ts.isParenthesizedExpression(obj)) obj = obj.expression; if (ts.isObjectLiteralExpression(obj)) { - if (obj.properties.length === 0) { + if (obj.properties.nodes.length === 0) { lowerer.noLowering( `${surface} with an empty expected object`, obj, @@ -1488,7 +1488,7 @@ function classifyThrowsExpected( // Duplicate keys keep the LAST value (JS object-literal semantics); // property order is preserved for argument evaluation. const byId = new Map(); - for (const prop of obj.properties) { + for (const prop of obj.properties.nodes) { const named = ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop) ? prop : null; if (!named || !(ts.isIdentifier(named.name) || ts.isStringLiteral(named.name))) { @@ -1585,18 +1585,18 @@ function lowerAssertThrows( ): IrExpr { const surface = doesNot ? "assert.doesNotThrow" : "assert.throws"; requireStatementPosition(lowerer, expr, surface); - if (expr.arguments.length < 1 || expr.arguments.length > 3) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 3) { lowerer.noLowering( - `${surface} with ${expr.arguments.length} arguments`, + `${surface} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${surface}(fn), ${surface}(fn, expected), and ${surface}(fn, expected, message)`, ); } - const fn = lowerer.lowerExpr(expr.arguments[0]!); + const fn = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (fn.type.kind !== "func" || fn.type.params.length !== 0) { lowerer.noLowering( `${surface} with this callback shape`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the callback must be a zero-parameter function", ); } @@ -1637,14 +1637,14 @@ function lowerAssertRejects( doesNot: boolean, ): IrExpr { const surface = doesNot ? "assert.doesNotReject" : "assert.rejects"; - if (expr.arguments.length < 1 || expr.arguments.length > 3) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 3) { lowerer.noLowering( - `${surface} with ${expr.arguments.length} arguments`, + `${surface} with ${expr.arguments.nodes.length} arguments`, expr, `the supported forms are ${doesNot ? "doesNotReject" : "rejects"}(fn), (fn, expected), and (fn, expected, message)`, ); } - const recv = lowerer.lowerExpr(expr.arguments[0]!); + const recv = lowerer.lowerExpr(expr.arguments.nodes[0]!); const recvIsPromise = recv.type.kind === "promise"; if ( !recvIsPromise && @@ -1652,7 +1652,7 @@ function lowerAssertRejects( ) { lowerer.noLowering( `${surface} with this first argument`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "an async (promise-returning) zero-parameter function or a promise are the lowered forms " + "(a bare thenable — an object with a then method — has no lowering: wrap it, e.g. Promise.resolve().then(() => thenable))", ); @@ -1728,7 +1728,7 @@ function refuseNoErrorShape( // statement of the same rule. lowerer.noLowering( `${surface} with an object-shape expectation`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "Node rejects this form when the callback fails — use a class, a regex, a validation callback, or a bare call", ); } @@ -1743,7 +1743,7 @@ function throwsArguments( expr: ts.CallExpression, loc: SrcLoc, ): { expected: ThrowsExpected; msg: IrExpr; hasMsg: IrExpr } { - const expectedNode = expr.arguments[1]; + const expectedNode = expr.arguments.nodes[1]; // A literal `undefined` expected counts as omitted (Node reads the // message from the THIRD argument in that spelling). const expected: ThrowsExpected = @@ -1752,20 +1752,20 @@ function throwsArguments( : { form: "bare" }; if (expected.form === "message") { if ( - expr.arguments.length === 3 && + expr.arguments.nodes.length === 3 && surface !== "assert.doesNotThrow" && surface !== "assert.doesNotReject" ) { lowerer.noLowering( `${surface} with a string expected argument AND a message`, - expr.arguments[2]!, + expr.arguments.nodes[2]!, "Node rejects this form (the string IS the message) — use an expected shape second", ); } - if (expr.arguments[2]) { + if (expr.arguments.nodes[2]) { // No-error assertions ignore the third value in the string-second // form, while ordinary argument evaluation still runs it once. - const ignored = lowerer.lowerExpr(expr.arguments[2]); + const ignored = lowerer.lowerExpr(expr.arguments.nodes[2]); const message = lowerer.declareHiddenLocal("%assertNoErrorMessage", STRING); return { expected, @@ -1784,7 +1784,7 @@ function throwsArguments( } return { expected, msg: expected.msg, hasMsg: expected.hasMsg }; } - const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments[2], loc); + const { msg, hasMsg } = lowerMessageArg(lowerer, expr.arguments.nodes[2], loc); return { expected, msg, hasMsg }; } @@ -2218,14 +2218,14 @@ function ifErrorLibFn( * an interned helper. Anything else fences. */ function lowerAssertIfError(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { requireStatementPosition(lowerer, expr, "assert.ifError"); - if (expr.arguments.length !== 1) { + if (expr.arguments.nodes.length !== 1) { lowerer.noLowering( - `assert.ifError with ${expr.arguments.length} arguments`, + `assert.ifError with ${expr.arguments.nodes.length} arguments`, expr, "the supported form is ifError(value)", ); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); if (argT && isUnitType(argT)) { // null/undefined can never throw, and a unit-typed expression has no diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index 1bd074dd7..c20f32480 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -81,10 +81,10 @@ function literalBooleanOption( fallback: boolean, api: string, ): boolean { - if (call.arguments.length === 0) return fallback; - const argumentNode = call.arguments[0]; + if (call.arguments.nodes.length === 0) return fallback; + const argumentNode = call.arguments.nodes[0]; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || argumentNode === undefined || ts.isSpreadElement(argumentNode) ) { @@ -101,7 +101,7 @@ function literalBooleanOption( return fallback; } let answer = fallback; - for (const member of argument.properties) { + for (const member of argument.properties.nodes) { const name = propertyName(member); if (name !== option) { const value = ts.isPropertyAssignment(member) ? member.initializer : null; @@ -305,7 +305,7 @@ export function checkForAwaitBinding( if (!isConst && !isLet) { lowerer.unsupported("SC1030", list, "'var' loop bindings in 'for await' (use const)"); } - const declaration = list.declarations[0]; + const declaration = list.declarations.nodes[0]; if (!declaration) throw new InternalCompilerError("for-await declaration list has no binding"); if (!ts.isIdentifier(declaration.name)) lowerer.unsupported("SC1031", declaration.name); return { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 5c1b47e6a..67ca01b3f 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -248,7 +248,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "Object" && lowerer.isStdlibGlobal(expr.expression, "Object") ) { - const object = lowerObjectConstructor(lowerer, expr.arguments, loc); + const object = lowerObjectConstructor(lowerer, expr.arguments.nodes, loc); if (object) return object; } @@ -537,7 +537,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "queueMicrotask" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { // Node: queueMicrotask() throws ERR_INVALID_ARG_TYPE at runtime // (the undefined callback) — the Dyn form delivers exactly that. return { @@ -548,7 +548,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { loc, }; } - const raw = expr.arguments[0]!; + const raw = expr.arguments.nodes[0]!; const cb = lowerer.lowerExpr(raw); if (cb.type.kind === "dyn") { return { kind: "libCall", fn: "timers.queueMicrotaskDyn", args: [cb], type: VOID, loc }; @@ -588,11 +588,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "structuredClone" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { return { kind: "libCall", fn: "dyn.cloneMissing", args: [], type: DYN, loc }; } - if (expr.arguments.length > 2) { - lowerer.noLowering(`structuredClone with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 2) { + lowerer.noLowering(`structuredClone with ${expr.arguments.nodes.length} arguments`, expr); } const toDynArg = (a: ts.Expression | undefined): IrExpr => { if (!a) return dynUndefinedExpr(loc); @@ -607,16 +607,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } return conv; }; - const valueNode = expr.arguments[0]!; + const valueNode = expr.arguments.nodes[0]!; // A NON-EMPTY transfer array of static values: nothing static is // transferable, so the call is Node's DataCloneError — decided here // (the list's values need no dyn representation to fail). An EMPTY // literal transfer list is a no-op member and drops. { - let optNode = expr.arguments[1]; + let optNode = expr.arguments.nodes[1]; while (optNode && ts.isParenthesizedExpression(optNode)) optNode = optNode.expression; if (optNode && ts.isObjectLiteralExpression(optNode)) { - const tr = optNode.properties.find( + const tr = optNode.properties.nodes.find( (p): p is ts.PropertyAssignment => ts.isPropertyAssignment(p) && p.name !== undefined && @@ -626,11 +626,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( tr && ts.isArrayLiteralExpression(tr.initializer) && - tr.initializer.elements.length > 0 + tr.initializer.elements.nodes.length > 0 ) { // An ArrayBuffer first member may be a Buffer pool slab, which // Node reports as untransferable. - const first = tr.initializer.elements[0]!; + const first = tr.initializer.elements.nodes[0]!; const firstType = ts.isSpreadElement(first) ? undefined : lowerer.typeOf(first); const parts = !firstType ? [] @@ -656,7 +656,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } } } - const optsArg = toDynArg(expr.arguments[1]); + const optsArg = toDynArg(expr.arguments.nodes[1]); // A DOMException value clones through its own runtime arm — the // typed result keeps the class (instanceof, .code, throwability). const valueT = lowerer.mapTypeOf(lowerer.typeOf(valueNode)); @@ -719,10 +719,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // constants ("", false, 0). Wrapper objects stay fenced except where // the receiver is immediately converted by a direct String method call. if (lowerer.isStdlibGlobal(expr.expression, "BigInt")) { - if (expr.arguments.length !== 1) { - lowerer.noLowering(`BigInt with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`BigInt with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; // Captured optional storage can change after TypeScript's guard. // BigInt consumes the stored value, including its nullish tag. const arg = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); @@ -745,10 +745,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { : lowerer.isStdlibGlobal(expr.expression, "Boolean") ? "Boolean" : "Number"; - if (expr.arguments.length > 1) { - lowerer.noLowering(`${name} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`${name} with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]; + const argNode = expr.arguments.nodes[0]; if (!argNode) { if (name === "String") return { kind: "strLit", value: "", type: STRING, loc }; if (name === "Boolean") return { kind: "boolLit", value: false, type: BOOL, loc }; @@ -815,10 +815,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const receiver = varRef(local.id, DYN, loc); const key = lowerer.lowerExprExpecting(access.argumentExpression, DYN); const callee: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc }; - const args = expr.arguments.map((arg) => + const args = expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(ts.isSpreadElement(arg) ? arg.expression : arg, DYN), ); - const spreads = expr.arguments.flatMap((arg, index) => + const spreads = expr.arguments.nodes.flatMap((arg, index) => ts.isSpreadElement(arg) ? [{ arg: index, what: arg.expression.getText() }] : [], ); return { @@ -852,7 +852,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // all (fs.watch, os.cpus, ...) fence with the module-qualified name. if (ts.isIdentifier(expr.expression)) { if (expr.expression.text === "Array" && lowerer.isStdlibGlobal(expr.expression, "Array")) { - return lowerArrayConstructor(lowerer, expr, expr.arguments); + return lowerArrayConstructor(lowerer, expr, expr.arguments.nodes); } // A call through a compile-time util.promisify projection. execFile // keeps its custom result helper; ordinary builtins route through @@ -900,9 +900,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (threadsImportOf(lowerer.program, expr.expression) === "publish") { // @scriptc/threads publish(value): the same value, its graph made // immortal and immutable (backend/llvm/publish.ts). - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) - lowerer.noLowering(`publish with ${expr.arguments.length} arguments`, expr); - const value = lowerer.lowerExpr(expr.arguments[0]!); + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + lowerer.noLowering(`publish with ${expr.arguments.nodes.length} arguments`, expr); + const value = lowerer.lowerExpr(expr.arguments.nodes[0]!); return { kind: "intrinsic", name: "threads.publish", args: [value], type: value.type, loc }; } const bi = lowerer.builtinImportOf(expr.expression); @@ -933,10 +933,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { expr.expression.text === "Symbol" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { - if (expr.arguments.length > 1) { - lowerer.noLowering(`Symbol with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`Symbol with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]; + const argNode = expr.arguments.nodes[0]; // A literal `undefined` argument IS the no-description form // (Symbol(undefined).description is undefined, like Symbol()). if (!argNode || (ts.isIdentifier(argNode) && argNode.text === "undefined")) { @@ -973,7 +973,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer, expr.expression.text, expr, - expr.arguments, + expr.arguments.nodes, loc, ); if (parsed !== null) return parsed; @@ -996,14 +996,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { const name = expr.expression.text; - if (expr.arguments.length !== 1) { + if (expr.arguments.nodes.length !== 1) { lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, ); } const loc = locOf(expr); - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; if (name === "decodeURIComponent" || name === "decodeURI") { const d = optionalCallValue(lowerer, argNode) ?? lowerer.lowerExpr(argNode); const s = lowerer.ensureString(d, argNode); @@ -1038,13 +1038,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined) ) { const name = expr.expression.text; - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { return { kind: "libCall", fn: "str.b64Missing", args: [], type: STRING, loc }; } - if (expr.arguments.length > 1) { - lowerer.noLowering(`${name} with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length > 1) { + lowerer.noLowering(`${name} with ${expr.arguments.nodes.length} arguments`, expr); } - const argNode = expr.arguments[0]!; + const argNode = expr.arguments.nodes[0]!; const v = lowerer.lowerExpr(argNode); let data: IrExpr; if (v.type.kind === "dyn") { @@ -1071,20 +1071,20 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // result exits to the declared static type. A user function // shadowing the name has a different, non-stdlib symbol. const islFn = lowerer.stdlibGlobalFnOf(expr.expression); - if (islFn && expr.arguments.length !== islFn.args.length) { + if (islFn && expr.arguments.nodes.length !== islFn.args.length) { const name = expr.expression.text; lowerer.noLowering( - `${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `${name} with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, ); } - if (islFn && expr.arguments.length === islFn.args.length) { + if (islFn && expr.arguments.nodes.length === islFn.args.length) { // The static forms above already took the exactly-typed argument // (a string for parseFloat, a number for isFinite): name the // argument type that refused the call, so the message states the // condition rather than reading as if the global itself were // unsupported. - const argText = loweredArgumentText(lowerer, expr.arguments[0]!); + const argText = loweredArgumentText(lowerer, expr.arguments.nodes[0]!); const exact = expr.expression.text === "isFinite" ? "number" : "string"; lowerer.refuseUnsupportedApi( `'${expr.expression.text}' with a '${argText}' argument`, @@ -1104,8 +1104,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // typeof tests) applies inside it, tsc's call-site narrowing types // the guarded branch, and a body construct with no caught lowering // fences per site at its own location. - if (ts.isIdentifier(expr.expression) && expr.arguments.length === 1) { - const caughtArg = lowerer.caughtLocalOf(expr.arguments[0]!); + if (ts.isIdentifier(expr.expression) && expr.arguments.nodes.length === 1) { + const caughtArg = lowerer.caughtLocalOf(expr.arguments.nodes[0]!); if (caughtArg) { const inlined = lowerCaughtPredicateCall(lowerer, expr, caughtArg); if (inlined) return inlined; @@ -1134,7 +1134,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // the declaration's value and applies through a runtime-built // argument list. Typed .ts spreads keep completeArgs' rest packing. const jsSpreadArgs = - expr.arguments.some((a) => ts.isSpreadElement(a)) && isJsSourceFile(expr.getSourceFile()); + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && + isJsSourceFile(expr.getSourceFile()); if (lowerer.isTopLevelFnSymbol(expr.expression) && !lowerer.peekLocal(expr.expression)) { // `import g = N.f; g()` — the alias's own source-order guards // (a no-op for every non-import= binding). @@ -1146,9 +1147,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (generic && !(jsSpreadArgs && generic.implicitParams)) return lowerer.lowerGenericCall(expr, generic); const sig = generic ? null : lowerer.fnSigOf(expr.expression); - if (sig && !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments))) { + if ( + sig && + !(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments.nodes)) + ) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); const symbol = lowerer.resolveValueSymbol(expr.expression); const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; // A bare call supplies undefined as this, even inside a callback @@ -1157,7 +1161,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( declaration && ts.isFunctionDeclaration(declaration) && - declaration.parameters.some(isThisParameter) + declaration.parameters.nodes.some(isThisParameter) ) { const callee = lowerer.lowerExpr(expr.expression); return reconcileOverloadReturn(lowerer, expr, { @@ -1250,7 +1254,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const sig = lowerer.fnSigOf(nsMember); if (sig) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); return reconcileOverloadReturn(lowerer, expr, { kind: "call", callee: sig.name, @@ -1313,7 +1317,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const sig = lowerer.fnSigOf(nameId); if (sig) { lowerer.noteEdge(sig.name); - const args = lowerer.completeArgs(expr.arguments, sig.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr); return reconcileOverloadReturn(lowerer, expr, { kind: "call", callee: sig.name, @@ -1532,13 +1536,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (intrinsic) return intrinsic; if (isJsSourceFile(expr.getSourceFile())) { const staticField = lowerStaticFieldRead(lowerer, expr.expression); - if (staticField?.type.kind === "dyn" && !expr.arguments.some(ts.isSpreadElement)) { + if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) { return { kind: "dynCall", callee: staticField, receiver: lowerer.lowerExprExpecting(expr.expression.expression, DYN), calleeName: v8CalleeText(expr.expression), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc: locOf(expr), }; @@ -1551,8 +1555,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr)); return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) }; } - if (callee.type.kind === "dyn" && !expr.arguments.some((arg) => ts.isSpreadElement(arg))) { - const args = expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + if ( + callee.type.kind === "dyn" && + !expr.arguments.nodes.some((arg) => ts.isSpreadElement(arg)) + ) { + const args = expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "dynCall", callee, @@ -1628,7 +1635,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ts.isElementAccessExpression(expr.expression) && !expr.expression.questionDotToken && !expr.questionDotToken && - expr.arguments.length === 0 + expr.arguments.nodes.length === 0 ) { const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression); if (iterator) return iterator; @@ -1680,7 +1687,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const key = raw.type.kind === "string" ? raw : lowerer.coerceInto(access.argumentExpression, raw, DYN); const callee: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc }; - const spread = expr.arguments.some(ts.isSpreadElement) + const spread = expr.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, expr, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -1693,7 +1700,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { callee, receiver, calleeName: access.getText(), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1739,7 +1746,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (virtual) lowerer.noteVirtualEdge(info, memberName); else lowerer.noteEdge(`%${found.declarer.def.name}.${memberName}`); const receiver = lowerer.lowerExpr(expr.expression.expression); - const args = lowerer.completeArgs(expr.arguments, found.sig.params, locOf(expr), expr); + const args = lowerer.completeArgs( + expr.arguments.nodes, + found.sig.params, + locOf(expr), + expr, + ); const callArgs = [ lowerer.upcastTo(receiver, virtual ? info.def.name : found.declarer.def.name), ...args, @@ -1794,7 +1806,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // function" TypeError. The result is dyn (checked per use like every // any-origin value). Spread arguments keep their fence. if (callee.type.kind === "dyn") { - if (expr.arguments.some((a) => ts.isSpreadElement(a))) { + if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) { // The runtime-arity lane: a dyn callee is already boxed — the // spread-marked dynCall applies through a fresh dyn argument // array (lowerSpreadArgsCall). Sources outside it keep the fence. @@ -1802,7 +1814,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (spreadServed) return spreadServed; lowerer.unsupported("SC1090", expr, "spread arguments in calls through 'unknown' values"); } - const args = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) @@ -1821,7 +1833,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee: lowerer.coerceToExpected(callee, DYN), calleeName: v8CalleeText(expr.expression), - args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1842,7 +1854,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // or marshals the callee and applies through a runtime-built argument // list (lowerSpreadArgsCall). Shapes outside its lanes fall through // to the historical fences. - if (expr.arguments.some((a) => ts.isSpreadElement(a))) { + if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) { const spreadServed = lowerSpreadArgsCall(lowerer, expr, callee, loc); if (spreadServed) return spreadServed; } @@ -1854,17 +1866,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // JS arity exactly (the thunk ignores extras). Result dyn, checked // per use like every any-origin value. if ( - (expr.arguments.length > callee.type.params.length || callee.type.rest === true) && + (expr.arguments.nodes.length > callee.type.params.length || callee.type.rest === true) && (isJsSourceFile(expr.getSourceFile()) || (callee.type.rest === true && callee.type.restAbi === undefined)) && - !expr.arguments.some((a) => ts.isSpreadElement(a)) && + !expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && canBoxFuncIntoDyn( callee.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id), ) ) { - const args = expr.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = expr.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); const calleeName = ts.isIdentifier(expr.expression) ? expr.expression.text : "value"; const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; return { kind: "dynCall", callee: boxed, calleeName, args, type: DYN, loc }; @@ -1887,14 +1899,15 @@ function lowerCheckedCallableValue( callee.type.kind !== "func" || !( (callee.type.rest === true && callee.type.restAbi === undefined) || - (isJsSourceFile(call.getSourceFile()) && call.arguments.length > callee.type.params.length) + (isJsSourceFile(call.getSourceFile()) && + call.arguments.nodes.length > callee.type.params.length) ) || !lowerer.dynConvertible(callee.type) ) return null; callee = { kind: "dynFrom", value: callee, type: DYN, loc }; } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { const spread = lowerSpreadArgsCall(lowerer, call, callee, loc); if (spread) return spread; lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); @@ -1903,7 +1916,7 @@ function lowerCheckedCallableValue( kind: "dynCall", callee, calleeName: v8CalleeText(call.expression), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -1918,18 +1931,19 @@ function lowerRegexMethodCallWithOptionalArg( if ( lowered?.kind !== "regexIntrinsic" || lowered.method !== "test" || - call.arguments.length !== 1 + call.arguments.nodes.length !== 1 ) return lowered; const subject = lowered.args[0]; if (subject?.type.kind !== "union" || lowerer.armTag(subject.type.unionId, UNDEFINED_T) < 0) return lowered; - if (lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!))?.kind !== "string") return lowered; + if (lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!))?.kind !== "string") + return lowered; const present = lowerer.stripUndefinedArm(subject.type); if (present.kind !== "string") return lowered; return { ...lowered, - args: [lowerer.ensureString(subject, call.arguments[0]!)], + args: [lowerer.ensureString(subject, call.arguments.nodes[0]!)], }; } @@ -1952,7 +1966,7 @@ function lowerBufferStaticCallWithNarrowedArg( // A callback can store a wider union than the checker sees at this use // (including an added undefined arm). Extract the exact arm proven by // control-flow narrowing, not merely the union with undefined removed. - const narrowed = lowerer.mapTypeOf(lowerer.typeOf(call.arguments[0]!)); + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[0]!)); if ( !narrowed || !( @@ -1964,7 +1978,7 @@ function lowerBufferStaticCallWithNarrowedArg( ) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm", ); } @@ -1976,7 +1990,7 @@ function lowerBufferStaticCallWithNarrowedArg( if (!helper) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "a Buffer constructor argument whose narrowed type is not a stored source arm", ); } @@ -2049,7 +2063,7 @@ function lowerStringMethodCallWithOptionalArgs( call: ts.CallExpression, access: ts.PropertyAccessExpression, receiver?: () => IrExpr, - argumentNodes: readonly ts.Expression[] = call.arguments, + argumentNodes: readonly ts.Expression[] = call.arguments.nodes, ): IrExpr | null { const lowered = lowerStringMethodCall(lowerer, call, access, receiver, argumentNodes); if (lowered?.kind !== "strIntrinsic") return lowered; @@ -2275,7 +2289,7 @@ function immediatePrimitiveWrapperToString(lowerer: Lowerer, node: ts.Expression !lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(wrapped.expression) ?? undefined) ) return null; - const args = wrapped.arguments ?? []; + const args = wrapped.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; const loc = locOf(wrapped); const primitive: IrExpr = @@ -2346,7 +2360,7 @@ function immediateObjectTagReceiver( (ts.isCallExpression(value) || ts.isNewExpression(value)) && ts.isIdentifier(value.expression) && (value.expression.text === "Array" || value.expression.text === "Error") && - (value.arguments?.length ?? 0) === 0 && + (value.arguments?.nodes.length ?? 0) === 0 && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { return { tag: value.expression.text, effect: { kind: "numLit", value: 0, type: F64, loc } }; @@ -2357,7 +2371,7 @@ function immediateObjectTagReceiver( lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { const name = value.expression.text; - const args = value.arguments ?? []; + const args = value.arguments?.nodes ?? []; // JavaScript dates use checked native handles, so their IR type alone // cannot recover the tag through an immediate Object(...) wrapper. if (name === "Date") return { tag: "Date", effect: lowerer.lowerExpr(value) }; @@ -2388,7 +2402,7 @@ function immediateObjectTagReceiver( value.expression.text === "Object" && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(value.expression) ?? undefined) ) { - const args = value.arguments ?? []; + const args = value.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const first = args[0]; const inner = @@ -2455,7 +2469,7 @@ function lowerArrayPrototypeBorrowCall( access.questionDotToken || access.name.text !== "call" || !ts.isPropertyAccessExpression(access.expression) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const method = access.expression; @@ -2470,10 +2484,10 @@ function lowerArrayPrototypeBorrowCall( ) return null; const loc = locOf(call); - const receiver = call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0]!, DYN) + const receiver = call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN) : (dynUndefinedExpr(loc) as IrExpr); - const args = call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "libCall", fn: "dyn.arrayProtoCall", @@ -2502,15 +2516,15 @@ function lowerObjectOwnPrototypeCall( prototype.name.text !== "prototype" || !lowerer.isStdlibGlobal(prototype.expression, "Object") || !lowerer.isStdlibMember(method) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; return lowerObjectOwnCall( lowerer, call, - call.arguments[0], - call.arguments[1], - call.arguments.slice(2), + call.arguments.nodes[0], + call.arguments.nodes[1], + call.arguments.nodes.slice(2), method.name.text, ); } @@ -2525,7 +2539,7 @@ function lowerObjectOwnMethodCall( call.questionDotToken || access.questionDotToken || (method !== "hasOwnProperty" && method !== "propertyIsEnumerable") || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; if (!lowerer.isStdlibMember(access)) { @@ -2536,8 +2550,8 @@ function lowerObjectOwnMethodCall( lowerer, call, access.expression, - call.arguments[0], - call.arguments.slice(1), + call.arguments.nodes[0], + call.arguments.nodes.slice(1), method, access, ); @@ -2791,13 +2805,13 @@ function lowerObjectPrototypeCall( prototype.name.text !== "prototype" || !lowerer.isStdlibGlobal(prototype.expression, "Object") || !lowerer.isStdlibMember(method) || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const loc = locOf(call); - if (call.arguments.length === 0) + if (call.arguments.nodes.length === 0) return { kind: "strLit", value: "[object Undefined]", type: STRING, loc }; - const receiverNode = call.arguments[0]!; + const receiverNode = call.arguments.nodes[0]!; const immediate = immediateObjectTagReceiver(lowerer, receiverNode); const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode); const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type); @@ -2817,7 +2831,7 @@ function lowerObjectPrototypeCall( stmts.push({ kind: "varDecl", localId: local.id, init: receiver, loc }); ref = varRef(local.id, receiver.type, loc); } - for (const extra of call.arguments.slice(1)) { + for (const extra of call.arguments.nodes.slice(1)) { stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(extra), loc: locOf(extra) }); } const result: IrExpr = @@ -2863,7 +2877,7 @@ function lowerDynamicStringPrototypeCall( }; const overrides: { node: ts.Expression; previous: IrExpr | undefined }[] = []; try { - for (const node of call.arguments.slice(1)) { + for (const node of call.arguments.nodes.slice(1)) { const undefinedValue = lowerStaticallyUndefinedArgument(lowerer, node); const value = undefinedValue ?? lowerer.lowerExpr(node); let ref: IrExpr; @@ -2889,7 +2903,7 @@ function lowerDynamicStringPrototypeCall( call, methodAccess, () => converted, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); return result ? { kind: "seqExpr", stmts, result, type: result.type, loc } : null; } finally { @@ -2918,7 +2932,7 @@ function lowerStringPrototypeCall( !lowerer.isStdlibMember(methodAccess) ) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; const entry = STR_METHODS[methodAccess.name.text]; const indexMethod = STRING_INDEX_METHODS.has(methodAccess.name.text) ? (methodAccess.name.text as "at" | "codePointAt") @@ -2941,12 +2955,12 @@ function lowerStringPrototypeCall( resultType, loc, ); - const receiverNode = call.arguments[0]; + const receiverNode = call.arguments.nodes[0]; if (!receiverNode) return nullishError; // Receiver coercion runs after .call has evaluated its arguments; keep // contextual wrapper lowering on forms without later method arguments. const immediateWrapper = - entry?.maxArgs === 0 && call.arguments.length === 1 + entry?.maxArgs === 0 && call.arguments.nodes.length === 1 ? immediatePrimitiveWrapperToString(lowerer, receiverNode) : null; const receiverValue = immediateWrapper ?? lowerer.lowerExpr(receiverNode); @@ -2960,7 +2974,10 @@ function lowerStringPrototypeCall( ?.arms.every((arm) => isUnitType(arm) || arm.kind === "void") ?? false)); if (nullish) { - const values = [receiverValue, ...call.arguments.slice(1).map((arg) => lowerer.lowerExpr(arg))]; + const values = [ + receiverValue, + ...call.arguments.nodes.slice(1).map((arg) => lowerer.lowerExpr(arg)), + ]; return { kind: "seqExpr", stmts: values @@ -2995,7 +3012,7 @@ function lowerStringPrototypeCall( // padding uses a helper that delays conversion until every argument is ready. const objectWithoutMethodArgs = entry?.maxArgs === 0 && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && (receiverType.kind === "record" || receiverType.kind === "array" || receiverType.kind === "object"); @@ -3006,7 +3023,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", lowerer.coerceToExpected(receiverValue, DYN), receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (!scalar && !objectWithoutMethodArgs) { @@ -3022,7 +3039,7 @@ function lowerStringPrototypeCall( indexMethod, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); if (!entry) return null; if (padding) { @@ -3032,7 +3049,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (entry.method === "split") { @@ -3041,7 +3058,7 @@ function lowerStringPrototypeCall( call, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } const receiver = lowerer.ensureString(receiverValue, receiverNode); @@ -3050,7 +3067,7 @@ function lowerStringPrototypeCall( call, methodAccess, () => receiver, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } @@ -3325,7 +3342,7 @@ function lowerDynReceiverMethodCall( type: DYN, loc, }; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments in callable property calls"); } return { @@ -3336,7 +3353,7 @@ function lowerDynReceiverMethodCall( callee: member, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }, @@ -3407,11 +3424,11 @@ function lowerDynReceiverMethodCall( if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && - call.arguments.length <= 1 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); - const hasArgument = call.arguments.length === 1; + const hasArgument = call.arguments.nodes.length === 1; const name = `%dyn.string.normalize:${hasArgument ? 1 : 0}`; if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const value = varRef("receiver", DYN, loc); @@ -3475,8 +3492,8 @@ function lowerDynReceiverMethodCall( callee: name, args: [ recv, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), ], type: DYN, @@ -3525,8 +3542,8 @@ function lowerDynReceiverMethodCall( } // The argument is a radix for bigint and an encoding for Buffer. // Preserve its runtime value so receiver dispatch can choose correctly. - if (access.name.text === "toString" && call.arguments.length <= 1) { - const argument = call.arguments[0]; + if (access.name.text === "toString" && call.arguments.nodes.length <= 1) { + const argument = call.arguments.nodes[0]; return { kind: "libCall", fn: "dyn.toString", @@ -3542,7 +3559,7 @@ function lowerDynReceiverMethodCall( // Every dyn kind's prototype chain declares valueOf, and the runtime // answers each kind's own: primitives and plain objects themselves, a // Date its time value, an own or inherited valueOf member its result. - if (access.name.text === "valueOf" && call.arguments.length === 0) { + if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) { return { kind: "libCall", fn: "dyn.valueOf", @@ -3658,13 +3675,13 @@ function lowerDynReceiverMethodCall( { kind: "varDecl", localId: receiverLocal.id, init: recv, loc }, { kind: "varDecl", localId: calleeLocal.id, init: stringRefusal(member), loc }, ]; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { const spread = lowerSpreadArgsCall(lowerer, call, callee, loc); if (spread?.kind === "dynCall") return { kind: "seqExpr", stmts, result: { ...spread, receiver }, type: DYN, loc }; lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } - const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); return { kind: "seqExpr", stmts, @@ -3966,7 +3983,7 @@ export function lowerDynDispatchMethodCall( lowerer.chainBlocked(call, access) ) return null; - if (call.arguments.some((arg) => ts.isSpreadElement(arg))) { + if (call.arguments.nodes.some((arg) => ts.isSpreadElement(arg))) { const loc = locOf(call); const local = lowerer.declareHiddenLocal("%spreadReceiver", DYN); const receiver = varRef(local.id, DYN, loc); @@ -3996,11 +4013,13 @@ export function lowerDynDispatchMethodCall( lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } const predicate = - method === "filter" && call.arguments[0] ? lowerer.lowerExpr(call.arguments[0]) : null; + method === "filter" && call.arguments.nodes[0] + ? lowerer.lowerExpr(call.arguments.nodes[0]) + : null; if (arrayReceiver && predicate?.type.kind === "func" && predicate.type.ret.kind === "void") { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "'.filter()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)", ); } @@ -4012,15 +4031,15 @@ export function lowerDynDispatchMethodCall( // at the callback boundary; valid callers preserve the original prefix. if ( method === "push" && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && ts.isIdentifier(access.expression) && ts.isExpressionStatement(call.parent) && ts.isBlock(call.parent.parent) ) { const receiverSymbol = lowerer.checker.getSymbolAtLocation(access.expression); const block = call.parent.parent; - const statementIndex = block.statements.indexOf(call.parent); - const next = statementIndex >= 0 ? block.statements[statementIndex + 1] : undefined; + const statementIndex = block.statements.nodes.indexOf(call.parent); + const next = statementIndex >= 0 ? block.statements.nodes[statementIndex + 1] : undefined; let returned = next && ts.isReturnStatement(next) ? next.expression : undefined; while (returned && ts.isParenthesizedExpression(returned)) returned = returned.expression; const apply = @@ -4030,7 +4049,7 @@ export function lowerDynDispatchMethodCall( returned.expression.name.text === "apply" ? returned : null; - const argsNode = apply?.arguments[1]; + const argsNode = apply?.arguments?.nodes[1]; const sameArgs = receiverSymbol !== undefined && argsNode !== undefined && @@ -4056,7 +4075,7 @@ export function lowerDynDispatchMethodCall( ), ) ) { - const value = lowerer.lowerExpr(call.arguments[0]!); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); if (value.type.kind !== "dyn" && !lowerer.dynConvertible(value.type)) { const saved = lowerer.declareHiddenLocal("%applyTail", DYN); return { @@ -4070,7 +4089,7 @@ export function lowerDynDispatchMethodCall( recv: { kind: "varRef", localId: saved.id, type: DYN, loc: locOf(access.expression) }, method, calleeName: access.getText(), - args: [dynUndefinedExpr(locOf(call.arguments[0]!))], + args: [dynUndefinedExpr(locOf(call.arguments.nodes[0]!))], type: DYN, loc: locOf(call), }, @@ -4080,7 +4099,7 @@ export function lowerDynDispatchMethodCall( } } } - const args = call.arguments.map((arg, i) => { + const args = call.arguments.nodes.map((arg, i) => { if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN); const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg); return undefinedArg @@ -4136,8 +4155,8 @@ function lowerArrayIsArrayCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Array") !== "isArray") return null; - if (call.arguments.length !== 1) return null; // the stdlib chokepoint fences - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; // the stdlib chokepoint fences + const argNode = call.arguments.nodes[0]!; const arg = lowerer.lowerExpr(argNode); const loc = locOf(call); if (arg.type.kind === "dyn") { @@ -4225,12 +4244,12 @@ function lowerCaughtPredicateCall( : undefined; if (!symbol || !decl || !decl.body) return null; if (!decl.type || !ts.isTypePredicateNode(decl.type)) return null; - if (decl.parameters.length !== 1) return null; - const param = decl.parameters[0]!; + if (decl.parameters.nodes.length !== 1) return null; + const param = decl.parameters.nodes[0]!; if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null; const paramSymbol = lowerer.checker.getSymbolAtLocation(param.name); if (!paramSymbol) return null; - const ret = decl.body.statements.length === 1 ? decl.body.statements[0] : undefined; + const ret = decl.body.statements.nodes.length === 1 ? decl.body.statements.nodes[0] : undefined; if (!ret || !ts.isReturnStatement(ret) || !ret.expression) { lowerer.unsupported( "SC1090", @@ -4272,8 +4291,8 @@ function lowerNumberToStringCall( if (lowerer.chainBlocked(call, access)) return null; if ( access.name.text !== "toString" || - call.arguments.length > 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) return null; const recvKind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; @@ -4289,8 +4308,8 @@ function lowerNumberToStringCall( fn: "dyn.toString", args: [ operand, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(locOf(call)), { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, ], @@ -4298,9 +4317,9 @@ function lowerNumberToStringCall( loc: locOf(call), }; } - if (call.arguments.length === 1) { + if (call.arguments.nodes.length === 1) { if (operand.type.kind !== "f64") return null; - const arg = call.arguments[0]!; + const arg = call.arguments.nodes[0]!; // A number radix needs no box: ToIntegerOrInfinity of a number is a // truncation, which the typed entry point applies itself. if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") { @@ -4349,7 +4368,7 @@ function lowerUnionToStringCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "toString" || call.arguments.length !== 0) return null; + if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null; const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (recvT?.kind !== "union") return null; if (!lowerer.isStdlibMember(access)) return null; @@ -4389,7 +4408,7 @@ function lowerDefaultToStringCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "toString" || call.arguments.length !== 0) return null; + if (access.name.text !== "toString" || call.arguments.nodes.length !== 0) return null; if (!lowerer.isStdlibMember(access)) return null; const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (!recvT) return null; @@ -4458,7 +4477,7 @@ function pureObjectToStringReceiver(node: ts.Expression): boolean { ) { e = e.expression; } - if (ts.isObjectLiteralExpression(e) && e.properties.length === 0) return true; + if (ts.isObjectLiteralExpression(e) && e.properties.nodes.length === 0) return true; return pureReceiverNode(e); } @@ -4474,9 +4493,9 @@ function lowerClassHasOwnPropertyCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; - if (access.name.text !== "hasOwnProperty" || call.arguments.length !== 1) return null; + if (access.name.text !== "hasOwnProperty" || call.arguments.nodes.length !== 1) return null; if (!ts.isIdentifier(access.expression)) return null; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; if (!ts.isStringLiteralLike(argNode)) return null; if (!lowerer.isStdlibMember(access)) return null; const sym = lowerer.resolveValueSymbol(access.expression); @@ -4484,7 +4503,7 @@ function lowerClassHasOwnPropertyCall( if (!info || !info.decl) return null; // builtin/runtime classes keep the fence const key = argNode.text; const own = new Set(["prototype", "name", "length"]); - for (const m of info.decl.members) { + for (const m of info.decl.members.nodes) { const isStatic = ts.canHaveModifiers(m) && (ts.getModifiers(m) ?? []).some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword); @@ -4564,8 +4583,8 @@ function lowerPrimitiveProtoCall( const recvKind = lowerer.mapTypeOf(lowerer.typeOf(recv))?.kind; if ( (name === "toFixed" || name === "toExponential") && - call.arguments.length <= 1 && - !call.arguments.some(ts.isSpreadElement) && + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) && isJsSourceFile(call.getSourceFile()) ) { const operand = lowerer.lowerExpr(recv); @@ -4578,14 +4597,14 @@ function lowerPrimitiveProtoCall( recv: lowerer.coerceToExpected(operand, DYN), method: name, calleeName: `${recv.getText()}.${name}`, - args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc: locOf(call), }; } if (recvKind !== "f64" && recvKind !== "bool" && recvKind !== "string") return null; const loc = locOf(call); - if (name === "toString" && call.arguments.length === 0) { + if (name === "toString" && call.arguments.nodes.length === 0) { const operand = lowerer.lowerExpr(recv); if (operand.type.kind === "string") return operand; // identity if (operand.type.kind !== "f64" && operand.type.kind !== "bool") return null; @@ -4594,16 +4613,16 @@ function lowerPrimitiveProtoCall( if ( (name === "toExponential" || name === "toFixed") && recvKind === "f64" && - call.arguments.length === 0 + call.arguments.nodes.length === 0 ) { const operand = lowerer.lowerExpr(recv); if (operand.type.kind !== "f64") return null; const fn = name === "toExponential" ? "num.toExponential" : "num.toFixed0"; return { kind: "libCall", fn, args: [operand], type: STRING, loc }; } - if (name === "toFixed" && recvKind === "f64" && call.arguments.length === 1) { + if (name === "toFixed" && recvKind === "f64" && call.arguments.nodes.length === 1) { const operand = lowerer.lowerExpr(recv); - let digits = lowerer.lowerExpr(call.arguments[0]!); + let digits = lowerer.lowerExpr(call.arguments.nodes[0]!); // The optional parameter also admits undefined. Exact unit values // become the default 0 after preserving any evaluation effects; an // optional number selects 0 at runtime through the same narrowed @@ -4642,7 +4661,7 @@ function lowerPrimitiveProtoCall( // host environment's default, which a compiled binary cannot carry), // other locales (ICU data the binary does not embed), options bags. if (name === "toLocaleString" && recvKind === "f64") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { lowerer.noLowering( "Number.prototype.toLocaleString without a locale", call, @@ -4650,7 +4669,7 @@ function lowerPrimitiveProtoCall( 'pass it explicitly: x.toLocaleString("en-US")', ); } - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { lowerer.noLowering( "Number.prototype.toLocaleString with an options bag", call, @@ -4658,7 +4677,7 @@ function lowerPrimitiveProtoCall( 'digits, grouping) — x.toLocaleString("en-US")', ); } - const locNode = call.arguments[0]!; + const locNode = call.arguments.nodes[0]!; if ( ts.isSpreadElement(locNode) || !ts.isStringLiteralLike(locNode) || @@ -4680,16 +4699,16 @@ function lowerPrimitiveProtoCall( if ( name === "charAt" && recvKind === "string" && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { const receiver = lowerer.lowerExpr(recv); if (receiver.type.kind !== "string") return null; - const idx = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const idx = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); return { kind: "strIntrinsic", method: "charAt", receiver, args: [idx], type: STRING, loc }; } - if (name !== "hasOwnProperty" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (name !== "hasOwnProperty" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (!ts.isStringLiteralLike(argNode)) return null; const key = argNode.text; if (recvKind === "string") { @@ -4737,7 +4756,7 @@ function implicitReceiverOverloadReturn( if ( !ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.some(ts.isSpreadElement) ) { return null; } @@ -4754,12 +4773,12 @@ function implicitReceiverOverloadReturn( if (!decl || !(ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) || decl.body) { return false; } - const params = decl.parameters.filter((param) => !isThisParameter(param)); + const params = decl.parameters.nodes.filter((param) => !isThisParameter(param)); const required = params.filter( (param) => !param.questionToken && !param.initializer && !param.dotDotDotToken, ).length; const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length; - return expr.arguments.length >= required && expr.arguments.length <= maximum; + return expr.arguments.nodes.length >= required && expr.arguments.nodes.length <= maximum; }); if (matches.length !== 1) return null; return lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(matches[0]!)); @@ -4791,7 +4810,7 @@ function isSoleAbstractMethod(decl: ts.FunctionDeclaration | ts.MethodDeclaratio const owner = decl.parent; if (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) return false; const name = decl.name.getText(); - return !owner.members.some( + return !owner.members.nodes.some( (member) => member !== decl && ts.isMethodDeclaration(member) && member.name.getText() === name, ); } @@ -4905,7 +4924,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp const loc = locOf(expr); const pieces = ts.isNoSubstitutionTemplateLiteral(expr.template) ? [expr.template] - : [expr.template.head, ...expr.template.templateSpans.map((s) => s.literal)]; + : [expr.template.head, ...expr.template.templateSpans.nodes.map((s) => s.literal)]; for (const p of pieces) { if (!templateEscapesValid(templateRawTextOf(p))) { lowerer.unsupported( @@ -4924,7 +4943,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp ); const values: readonly ts.Expression[] = ts.isNoSubstitutionTemplateLiteral(expr.template) ? [] - : expr.template.templateSpans.map((s) => s.expression); + : expr.template.templateSpans.nodes.map((s) => s.expression); // Direct call of a top-level declared function — the plain-call fast // path with the strings array as the leading completed argument. if (ts.isIdentifier(expr.tag) && !lowerer.isSelfReference(expr.tag)) { @@ -5167,7 +5186,7 @@ function markModuleNsHandlerParams( let e = handler; while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isArrowFunction(e) && !ts.isFunctionExpression(e)) return; - for (const p of e.parameters) { + for (const p of e.parameters.nodes) { if (ts.isIdentifier(p.name) && !p.dotDotDotToken && !p.initializer) { lowerer.moduleNsParamOverrides.set(p, type); } @@ -5210,15 +5229,15 @@ export function lowerPromiseMethodCall( ); }; const passthrough = - call.arguments.length === 0 || - (call.arguments.length === 1 && isAbsentHandler(call.arguments[0])); + call.arguments.nodes.length === 0 || + (call.arguments.nodes.length === 1 && isAbsentHandler(call.arguments.nodes[0])); if ( - call.arguments.length !== 1 && + call.arguments.nodes.length !== 1 && !passthrough && - !(member === "then" && call.arguments.length === 2) + !(member === "then" && call.arguments.nodes.length === 2) ) { lowerer.noLowering( - `${member} with ${call.arguments.length} arguments`, + `${member} with ${call.arguments.nodes.length} arguments`, call, member === "then" ? "the supported form takes a fulfillment handler and an optional rejection handler" @@ -5293,9 +5312,9 @@ export function lowerPromiseMethodCall( } if (member === "then") { - if (call.arguments.length === 2) { - const fulfilledNode = call.arguments[0]!; - const rejectedNode = call.arguments[1]!; + if (call.arguments.nodes.length === 2) { + const fulfilledNode = call.arguments.nodes[0]!; + const rejectedNode = call.arguments.nodes[1]!; const fulfilledAbsent = isAbsentHandler(fulfilledNode); const rejectedAbsent = isAbsentHandler(rejectedNode); const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -5542,8 +5561,9 @@ export function lowerPromiseMethodCall( // contextual type spelled (a module-namespace type has no mapping — // the handle is the value's only story, isIslandExpr's local rule). - if (inner.kind === "moduleNs") markModuleNsHandlerParams(lowerer, call.arguments[0]!, inner); - let cb = lowerer.lowerExpr(call.arguments[0]!); + if (inner.kind === "moduleNs") + markModuleNsHandlerParams(lowerer, call.arguments.nodes[0]!, inner); + let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic // instantiation typed the parameter, but the settled value is a @@ -5585,7 +5605,7 @@ export function lowerPromiseMethodCall( } lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, `then handlers receiving '${lowerer.fmt(v.type)}' values through an untyped handler (the settled value cannot cross the checked-dynamic tree boundary)`, ); }; @@ -5623,7 +5643,7 @@ export function lowerPromiseMethodCall( value: { kind: "dynCall", callee: { kind: "varRef", localId: cbLocal.id, type: DYN, loc }, - calleeName: jsFuncNameOf(call.arguments[0]!, lowerer) ?? "onFulfilled", + calleeName: jsFuncNameOf(call.arguments.nodes[0]!, lowerer) ?? "onFulfilled", args: handlerArgs, type: DYN, loc, @@ -5660,7 +5680,7 @@ export function lowerPromiseMethodCall( if (cb.type.kind !== "func" || cb.type.params.length > 1) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "then handlers with more than one parameter (the two-argument onRejected form has no lowering — chain .catch(...) instead)", ); } @@ -5672,7 +5692,7 @@ export function lowerPromiseMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, `then handlers whose parameter is not the settled value's type (expected '${lowerer.fmt(inner)}', got '${lowerer.fmt(param)}')`, ); } @@ -5768,7 +5788,7 @@ export function lowerPromiseMethodCall( } if (member === "finally") { - const cb = lowerer.lowerExpr(call.arguments[0]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[0]!); if (lowerer.dynConvertible(promT) && lowerer.dynConvertible(cb.type)) { const result: IrExpr = { kind: "dynInvoke", @@ -5784,7 +5804,7 @@ export function lowerPromiseMethodCall( if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "finally callbacks with parameters or a return value (use () => { ... })", ); } @@ -5878,7 +5898,7 @@ export function lowerPromiseMethodCall( (isJsSourceFile(call.getSourceFile()) && (inner.kind === "void" || lowerer.dynConvertible(inner)))) ) { - let cb = lowerer.lowerExpr(call.arguments[0]!); + let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( cb.type.kind === "func" && canBoxFuncIntoDyn( @@ -5929,7 +5949,7 @@ export function lowerPromiseMethodCall( value: { kind: "dynCall", callee: { kind: "varRef", localId: cbLocal.id, type: DYN, loc }, - calleeName: jsFuncNameOf(call.arguments[0]!, lowerer) ?? "onRejected", + calleeName: jsFuncNameOf(call.arguments.nodes[0]!, lowerer) ?? "onRejected", args: [ { kind: "caughtToDyn", @@ -5978,23 +5998,23 @@ export function lowerPromiseMethodCall( // .catch: the handler must be INLINE — its parameter becomes the // catch binding. - let handlerNode: ts.Expression = call.arguments[0]!; + let handlerNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(handlerNode)) handlerNode = handlerNode.expression; if (!ts.isArrowFunction(handlerNode) && !ts.isFunctionExpression(handlerNode)) { lowerer.unsupported( "SC1090", - call.arguments[0]!, + call.arguments.nodes[0]!, "catch handlers that are not inline function literals (the handler's parameter " + "becomes a typed-catch binding, which only an inline `(e) => ...` can receive)", ); } - if (handlerNode.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (handlerNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", handlerNode, "async catch handlers"); } - if (handlerNode.parameters.length > 1) { + if (handlerNode.parameters.nodes.length > 1) { lowerer.unsupported("SC1090", handlerNode, "catch handlers with more than one parameter"); } - const param = handlerNode.parameters[0]; + const param = handlerNode.parameters.nodes[0]; if (param && (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer)) { lowerer.unsupported("SC1062", param); } @@ -6052,7 +6072,7 @@ export function lowerPromiseMethodCall( } const hb = handlerNode.body; if (ts.isBlock(hb)) { - catchBody = lowerer.lowerStmts(hb.statements); + catchBody = lowerer.lowerStmts(hb.statements.nodes); } else if (R.kind === "void") { catchBody = [{ kind: "exprStmt", expr: lowerer.lowerExpr(hb), loc: locOf(hb) }]; } else { @@ -6144,9 +6164,9 @@ export function lowerFilterNarrowCall( const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); if (receiverIr?.kind !== "array") return null; if (!lowerer.isStdlibMember(access)) return null; - if (call.arguments.length !== 1) return null; // the generic path's arity fence + if (call.arguments.nodes.length !== 1) return null; // the generic path's arity fence const elem = receiverIr.elem; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const isBooleanArg = @@ -6379,21 +6399,21 @@ function lowerSymbolStaticCall( const member = access.name.text; const loc = locOf(call); if (member === "for") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Symbol.for with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Symbol.for with ${call.arguments.nodes.length} arguments`, call); } - const key = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const key = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: "sym.for", args: [key], type: SYMBOL_T, loc }; } if (member === "keyFor") { - if (call.arguments.length !== 1) { - lowerer.noLowering(`Symbol.keyFor with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1) { + lowerer.noLowering(`Symbol.keyFor with ${call.arguments.nodes.length} arguments`, call); } - const sym = lowerer.lowerExpr(call.arguments[0]!); + const sym = lowerer.lowerExpr(call.arguments.nodes[0]!); if (sym.type.kind !== "symbol") { lowerer.noLowering( `Symbol.keyFor of a '${lowerer.fmt(sym.type)}' value`, - call.arguments[0]!, + call.arguments.nodes[0]!, "the argument must be symbol-typed", ); } @@ -6421,12 +6441,12 @@ function lowerBigIntStaticCall( if (!lowerer.isStdlibGlobal(access.expression, "BigInt")) return null; const name = access.name.text; if (name !== "asUintN" && name !== "asIntN") return null; - if (call.arguments.length !== 2) { - lowerer.noLowering(`BigInt.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2) { + lowerer.noLowering(`BigInt.${name} with ${call.arguments.nodes.length} arguments`, call); } const loc = locOf(call); - const bits = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const value = lowerer.lowerExprExpecting(call.arguments[1]!, BIGINT_T); + const bits = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BIGINT_T); return { kind: "libCall", fn: name === "asUintN" ? "bigint.asUintN" : "bigint.asIntN", @@ -6447,11 +6467,11 @@ function lowerBigIntMethodCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "bigint") return null; - if (name === "valueOf" && call.arguments.length === 0) return receiver; - if (name === "toString" && call.arguments.length <= 1) { + if (name === "valueOf" && call.arguments.nodes.length === 0) return receiver; + if (name === "toString" && call.arguments.nodes.length <= 1) { const defaultRadix: IrExpr = { kind: "numLit", value: 10, type: F64, loc }; - const radix: IrExpr = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], F64, defaultRadix) + const radix: IrExpr = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], F64, defaultRadix) : defaultRadix; return { kind: "libCall", fn: "bigint.toString", args: [receiver, radix], type: STRING, loc }; } @@ -6472,12 +6492,12 @@ function lowerSymbolMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "toString" && call.arguments.length === 0) { + if (name === "toString" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "symbol") return null; return { kind: "libCall", fn: "sym.toString", args: [receiver], type: STRING, loc }; } - if (name === "valueOf" && call.arguments.length === 0) { + if (name === "valueOf" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "symbol") return null; return receiver; @@ -6720,12 +6740,12 @@ function lowerRegExpStaticCall( if (!lowerer.isStdlibGlobal(access.expression, "RegExp")) return null; if (access.name.text !== "escape") return null; if (!lowerer.isStdlibMember(access)) return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`RegExp.escape with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`RegExp.escape with ${call.arguments.nodes.length} arguments`, call); } - const arg = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const arg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); if (arg.type.kind !== "string") - lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!)); + lowerer.badType(call.arguments.nodes[0]!, lowerer.typeOf(call.arguments.nodes[0]!)); return { kind: "libCall", fn: "regexp.escape", args: [arg], type: STRING, loc: locOf(call) }; } @@ -6761,7 +6781,7 @@ function lowerIntlNumberFormatCall( ) { return null; } - ctorArgs = recv.arguments ?? []; + ctorArgs = recv.arguments?.nodes ?? []; } else { return null; } @@ -6793,13 +6813,13 @@ function lowerIntlNumberFormatCall( "everything else is ICU data the binary does not carry", ); } - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering( - `Intl.NumberFormat("en-US").format with ${call.arguments.length} arguments`, + `Intl.NumberFormat("en-US").format with ${call.arguments.nodes.length} arguments`, call, ); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; if (lowerer.mapTypeOf(lowerer.typeOf(argNode))?.kind !== "f64") { lowerer.noLowering( `Intl.NumberFormat("en-US").format over a '${lowerer.checker.typeToString(lowerer.typeOf(argNode))}'`, @@ -6848,10 +6868,10 @@ function lowerObjectStaticCall( (member === "getOwnPropertyNames" || member === "getOwnPropertySymbols" || member === "getOwnPropertyDescriptors") && - call.arguments.length === 1 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length === 1 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - let object = lowerer.lowerExpr(call.arguments[0]!); + let object = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( object.type.kind === "string" || object.type.kind === "f64" || @@ -6875,12 +6895,12 @@ function lowerObjectStaticCall( } if ( (member === "getPrototypeOf" || member === "setPrototypeOf") && - call.arguments.length === (member === "getPrototypeOf" ? 1 : 2) && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length === (member === "getPrototypeOf" ? 1 : 2) && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - let object = lowerer.lowerExpr(call.arguments[0]!); + let object = lowerer.lowerExpr(call.arguments.nodes[0]!); if (object.type.kind === "func") { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); object = { kind: "dynFrom", value: object, @@ -6897,7 +6917,7 @@ function lowerObjectStaticCall( (id) => lowerer.unions.get(id), ) ) - object = lowerer.coerceInto(call.arguments[0]!, object, DYN); + object = lowerer.coerceInto(call.arguments.nodes[0]!, object, DYN); if (object.type.kind === "dyn") { const loc = locOf(call); if (member === "getPrototypeOf") { @@ -6919,7 +6939,7 @@ function lowerObjectStaticCall( ...(identityOnly ? { prototypeIdentityOnly: true as const } : {}), }; } - const prototype = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const prototype = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (prototype.type.kind === "dyn") return { kind: "libCall", @@ -6930,8 +6950,8 @@ function lowerObjectStaticCall( }; } } - if (member === "getOwnPropertyDescriptor" && call.arguments.length === 2) { - const [receiver, key] = call.arguments; + if (member === "getOwnPropertyDescriptor" && call.arguments.nodes.length === 2) { + const [receiver, key] = call.arguments.nodes; if ( receiver && key && @@ -6971,16 +6991,19 @@ function lowerObjectStaticCall( // is (any, any) — and JS evaluates the arguments either way). if (member === "is") { - if (call.arguments.length !== 2 || call.arguments.some((a) => ts.isSpreadElement(a))) { + if ( + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { lowerer.noLowering( - `Object.is with ${call.arguments.length} arguments`, + `Object.is with ${call.arguments.nodes.length} arguments`, call, "exactly two arguments are the lowered form (JS treats a missing one as undefined — pass it explicitly)", ); } const loc = locOf(call); - const leftNode = call.arguments[0]!; - const rightNode = call.arguments[1]!; + const leftNode = call.arguments.nodes[0]!; + const rightNode = call.arguments.nodes[1]!; const left = lowerer.lowerExpr(leftNode); const right = lowerer.lowerExpr(rightNode); const lk = left.type.kind; @@ -7106,18 +7129,18 @@ function lowerObjectStaticCall( // Native checked objects retain a live prototype and can install an // own-property descriptor table during creation. if (member === "create") { - if (call.arguments.some((a) => ts.isSpreadElement(a))) { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { lowerer.noLowering("Object.create with spread arguments", call); } - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`Object.create with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`Object.create with ${call.arguments.nodes.length} arguments`, call); } const loc = locOf(call); - let protoNode: ts.Expression = call.arguments[0]!; + let protoNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(protoNode)) protoNode = protoNode.expression; const nullProto = protoNode.kind === ts.SyntaxKind.NullKeyword; - if (nullProto && call.arguments.length === 1) { + if (nullProto && call.arguments.nodes.length === 1) { return { kind: "libCall", fn: "dyn.objCreateNullProto", args: [], type: DYN, loc }; } let proto = nullProto @@ -7150,9 +7173,9 @@ function lowerObjectStaticCall( ) proto = lowerer.coerceInto(protoNode, proto, DYN); if (proto.type.kind === "dyn") { - if (call.arguments.length === 1) + if (call.arguments.nodes.length === 1) return { kind: "libCall", fn: "dyn.objCreate", args: [proto], type: DYN, loc }; - const descriptors = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const descriptors = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (descriptors.type.kind === "dyn") return { kind: "libCall", @@ -7171,10 +7194,10 @@ function lowerObjectStaticCall( if (prototype) return prototype; if ( isJsSourceFile(call.getSourceFile()) && - call.arguments.length > 0 && - !call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 0 && + !call.arguments.nodes.some(ts.isSpreadElement) ) { - const target = tryLowerExpression(lowerer, call.arguments[0]!); + const target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if ( target && ((target.type.kind === "func" && lowerer.dynConvertible(target.type)) || @@ -7188,7 +7211,9 @@ function lowerObjectStaticCall( lowerer.coerceToExpected(target, DYN), { kind: "dynArrLit", - elems: call.arguments.slice(1).map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + elems: call.arguments.nodes + .slice(1) + .map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }, @@ -7205,14 +7230,17 @@ function lowerObjectStaticCall( // exactly the source literal's properties, which IS the source // literal evaluated (both fresh, no alias can tell them apart). // Everything else keeps the spread hint (stdlibMemberFence). - if (call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) { - let target: ts.Expression = call.arguments[0]!; + if ( + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + let target: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(target)) target = target.expression; - let source: ts.Expression = call.arguments[1]!; + let source: ts.Expression = call.arguments.nodes[1]!; while (ts.isParenthesizedExpression(source)) source = source.expression; if ( ts.isObjectLiteralExpression(target) && - target.properties.length === 0 && + target.properties.nodes.length === 0 && ts.isObjectLiteralExpression(source) ) { return lowerer.lowerExpr(source); @@ -7232,17 +7260,20 @@ function lowerObjectStaticCall( // optional field must not erase the target's value — Node copies own // keys only — while an explicit `k: undefined` does). Everything // else keeps the spread hint. - if (call.arguments.length === 2 && !call.arguments.some((a) => ts.isSpreadElement(a))) { - const tProbe = tryLowerExpression(lowerer, call.arguments[0]!); - const sProbe = tryLowerExpression(lowerer, call.arguments[1]!); + if ( + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) { + const tProbe = tryLowerExpression(lowerer, call.arguments.nodes[0]!); + const sProbe = tryLowerExpression(lowerer, call.arguments.nodes[1]!); // CHECKED-DYNAMIC target and source (the JS file-scope // object-literal identity story): the runtime dyn copy — own // members of the source land on the target, which returns. if (tProbe?.type.kind === "dyn") { const loc = locOf(call); - const target = lowerer.lowerExpr(call.arguments[0]!); - const rawSource = lowerer.lowerExpr(call.arguments[1]!); - fenceSymbolFieldCopy(lowerer, call.arguments[1]!, rawSource.type); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const rawSource = lowerer.lowerExpr(call.arguments.nodes[1]!); + fenceSymbolFieldCopy(lowerer, call.arguments.nodes[1]!, rawSource.type); const source = lowerer.coerceToExpected(rawSource, DYN); if (target.type.kind === "dyn" && source.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc }; @@ -7266,8 +7297,8 @@ function lowerObjectStaticCall( }); if (ok) { const loc = locOf(call); - const target = lowerer.lowerExpr(call.arguments[0]!); - const source = lowerer.lowerExpr(call.arguments[1]!); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const source = lowerer.lowerExpr(call.arguments.nodes[1]!); if (target.type.kind === "record" && source.type.kind === "record") { const helper = recordAssignHelper( lowerer, @@ -7299,15 +7330,17 @@ function lowerObjectStaticCall( // unobservable), or a nullish unit (Node's ToObject TypeError // throws at the call, catchably); aliased record targets keep the // fence — their identity could not survive the conversion. - if (call.arguments.length >= 1 && !ts.isSpreadElement(call.arguments[0]!)) { - let targetNode: ts.Expression = call.arguments[0]!; + if (call.arguments.nodes.length >= 1 && !ts.isSpreadElement(call.arguments.nodes[0]!)) { + let targetNode: ts.Expression = call.arguments.nodes[0]!; while (ts.isParenthesizedExpression(targetNode)) targetNode = targetNode.expression; const freshLiteralTarget = ts.isObjectLiteralExpression(targetNode); - const tProbe = freshLiteralTarget ? null : tryLowerExpression(lowerer, call.arguments[0]!); + const tProbe = freshLiteralTarget + ? null + : tryLowerExpression(lowerer, call.arguments.nodes[0]!); const tKind = tProbe?.type.kind; if (freshLiteralTarget || tKind === "dyn" || tKind === "nullT" || tKind === "undefinedT") { const loc = locOf(call); - const target = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + const target = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); if (target.type.kind === "dyn") { const t = lowerer.declareHiddenLocal("%oat", DYN); const p = lowerer.declareHiddenLocal("%oap", DYN); @@ -7327,7 +7360,7 @@ function lowerObjectStaticCall( // SINGLE LAST argument; every other spread position drives // the real iterator protocol, whose failure describes the // value — the two runtime variants, picked here by position. - const sources = call.arguments.slice(1); + const sources = call.arguments.nodes.slice(1); const spreadCount = sources.filter((a) => ts.isSpreadElement(a)).length; let ok = true; for (let i = 0; i < sources.length; i++) { @@ -7396,12 +7429,12 @@ function lowerObjectStaticCall( return null; } if ( - (member === "defineProperty" && call.arguments.length === 3) || - (member === "getOwnPropertyDescriptor" && call.arguments.length === 2) + (member === "defineProperty" && call.arguments.nodes.length === 3) || + (member === "getOwnPropertyDescriptor" && call.arguments.nodes.length === 2) ) { - if (call.arguments.some((a) => ts.isSpreadElement(a))) return null; - let target = tryLowerExpression(lowerer, call.arguments[0]!); - const targetNode = call.arguments[0]!; + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) return null; + let target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); + const targetNode = call.arguments.nodes[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 = @@ -7425,7 +7458,7 @@ function lowerObjectStaticCall( target = lowerer.coerceToExpected(target, DYN); } // Error instances share a live property table with their checked view. - const keyNode = call.arguments[1]!; + const keyNode = call.arguments.nodes[1]!; if ( target?.type.kind === "object" && target.type.className !== "%DOMException" && @@ -7436,12 +7469,12 @@ function lowerObjectStaticCall( target = lowerer.coerceToExpected(target, DYN); } if (target?.type.kind === "func" && lowerer.dynConvertible(target.type)) { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); target = { kind: "dynFrom", value: target, type: DYN, - loc: locOf(call.arguments[0]!), + loc: locOf(call.arguments.nodes[0]!), ...(fnName !== null ? { fnName } : {}), }; } @@ -7452,8 +7485,8 @@ function lowerObjectStaticCall( if (!target || (target.type.kind !== "dyn" && !isUnitType(target.type) && !descriptorPrimitive)) return null; if (target.type.kind !== "dyn") - target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) }; - const key = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; + const key = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (key.type.kind !== "dyn") return null; if (member === "getOwnPropertyDescriptor") { return { @@ -7464,7 +7497,7 @@ function lowerObjectStaticCall( loc: locOf(call), }; } - const descriptor = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + const descriptor = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN); if (descriptor.type.kind !== "dyn") return null; return { kind: "libCall", @@ -7483,12 +7516,12 @@ function lowerObjectStaticCall( // static shapes have no property table to extend. if ( member === "defineProperties" && - call.arguments.length === 2 && - !call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { const prototype = lowerClassPrototypeDescriptors(lowerer, call, member); if (prototype) return prototype; - let target = tryLowerExpression(lowerer, call.arguments[0]!); + let target = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (target) { const native = lowerClassDataDescriptor(lowerer, call, member, target); if (native) return native; @@ -7506,20 +7539,20 @@ function lowerObjectStaticCall( (id) => lowerer.unions.get(id), ) ) { - const fnName = jsFuncNameOf(call.arguments[0]!, lowerer); + const fnName = jsFuncNameOf(call.arguments.nodes[0]!, lowerer); target = { kind: "dynFrom", value: target, type: DYN, - loc: locOf(call.arguments[0]!), + loc: locOf(call.arguments.nodes[0]!), ...(fnName !== null ? { fnName } : {}), }; } if (target && isUnitType(target.type)) { - target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments[0]!) }; + target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) }; } if (target?.type.kind === "dyn") { - const descs = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); + const descs = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); if (descs.type.kind === "dyn") { return { kind: "libCall", @@ -7534,10 +7567,10 @@ function lowerObjectStaticCall( } if ( ["preventExtensions", "isExtensible", "seal", "isSealed"].includes(member) && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { - const value = lowerer.lowerExpr(call.arguments[0]!); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); if ( value.type.kind === "string" || value.type.kind === "f64" || @@ -7581,15 +7614,15 @@ function lowerObjectStaticCall( // other reference can write). Primitives pass through per ES2015. // Aliased objects keep a fence: a later write through the original // reference would need the runtime frozen bit (strict mode throws). - if (member === "isFrozen" && call.arguments.length === 1) { - const value = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + if (member === "isFrozen" && call.arguments.nodes.length === 1) { + const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); return { kind: "libCall", fn: "dyn.isFrozen", args: [value], type: BOOL, loc: locOf(call) }; } if (member === "freeze") { - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { - lowerer.noLowering(`Object.freeze with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { + lowerer.noLowering(`Object.freeze with ${call.arguments.nodes.length} arguments`, call); } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let inner: ts.Expression = argNode; while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; @@ -7644,11 +7677,11 @@ function lowerObjectStaticCall( // do too. if ( member === "hasOwn" && - call.arguments.length === 2 && - !call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.length === 2 && + !call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { - const recvNode = call.arguments[0]!; - const keyNode = call.arguments[1]!; + const recvNode = call.arguments.nodes[0]!; + const keyNode = call.arguments.nodes[1]!; const probed = tryLowerExpression(lowerer, recvNode); const arrayKey = probed?.type.kind === "array" ? lowerer.lowerExpr(keyNode) : null; if (probed?.type.kind === "array" && arrayKey?.type.kind === "f64") { @@ -7706,8 +7739,9 @@ function lowerObjectStaticCall( return { kind: "call", callee: helper, args: [receiver, key], type: BOOL, loc }; } if (member !== "keys" && member !== "values" && member !== "entries") return null; - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const argNode = call.arguments.nodes[0]!; // util.types and node:util/types list Node's pinned key census // (UTIL_TYPES_KEYS): the object's own keys in Node's order, or the module // namespace's export names sorted with "default". values/entries would @@ -8288,11 +8322,12 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE const shape = lowerer.shapes.get(mapped.shapeId); const callField = shape?.fields.find((f) => f.name === "%call"); if (!shape || !callField || callField.type.kind !== "func") return null; - if (call.arguments.length < 2 || call.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (call.arguments.nodes.length < 2 || call.arguments.nodes.some((a) => ts.isSpreadElement(a))) + return null; const loc = locOf(call); const values = new Map(); - values.set("%call", lowerer.lowerExprExpecting(call.arguments[0]!, callField.type)); - for (const argNode of call.arguments.slice(1)) { + values.set("%call", lowerer.lowerExprExpecting(call.arguments.nodes[0]!, callField.type)); + for (const argNode of call.arguments.nodes.slice(1)) { let src: ts.Expression = argNode; while (ts.isParenthesizedExpression(src) || ts.isAsExpression(src) || ts.isTypeAssertion(src)) src = src.expression; @@ -8303,7 +8338,7 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE "Object.assign sources other than plain object literals when building a function-with-properties value", ); } - for (const prop of src.properties) { + for (const prop of src.properties.nodes) { const nameOk = (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)); @@ -8449,7 +8484,7 @@ export function lowerRecordFieldCall( ) { const receiver = target && receiverLocal ? lowerer.coerceToExpected(target.obj, DYN) : undefined; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -8463,7 +8498,7 @@ export function lowerRecordFieldCall( callee, ...(receiver ? { receiver } : {}), calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8534,7 +8569,7 @@ function lowerUrlStaticCall( if (callee.name.text === "canParse" || callee.name.text === "parse") { return lowerUrlFactory(lowerer, call, callee.name.text); } - if (callee.name.text !== "revokeObjectURL" || call.arguments.length !== 0) return null; + if (callee.name.text !== "revokeObjectURL" || call.arguments.nodes.length !== 0) return null; return nodeThrowExpr( 1, "ERR_MISSING_ARGS", @@ -8577,8 +8612,8 @@ function lowerInlineArrowBind( ): IrExpr | null { if ( access.name.text !== "bind" || - call.arguments.length > 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length > 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) return null; let arrow = access.expression; @@ -8597,8 +8632,8 @@ function lowerInlineArrowBind( return null; const type = callee.type; const loc = locOf(call); - const firstDefault = arrow.parameters.findIndex((p) => p.initializer !== undefined); - const length = firstDefault < 0 ? arrow.parameters.length : firstDefault; + const firstDefault = arrow.parameters.nodes.findIndex((p) => p.initializer !== undefined); + const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; @@ -8691,7 +8726,7 @@ function lowerInlineArrowBind( const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: callee, loc }]; // The receiver is ignored by the arrow, but its effects and throws // happen after creating the arrow and before returning the bound value. - if (call.arguments[0]) stmts.push(lowerer.lowerExprStatement(call.arguments[0])); + if (call.arguments.nodes[0]) stmts.push(lowerer.lowerExprStatement(call.arguments.nodes[0])); const result: IrExpr = { kind: "call", callee: name, @@ -8712,8 +8747,8 @@ function lowerArrowFunctionApply( ): IrExpr | null { if ( access.name.text !== "apply" || - call.arguments.length !== 2 || - call.arguments.some(ts.isSpreadElement) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes.some(ts.isSpreadElement) || arrowSignatureOf(lowerer, access.expression) === null ) { return null; @@ -8731,14 +8766,14 @@ function lowerArrowFunctionApply( ) { return null; } - const argsList = lowerer.lowerExpr(call.arguments[1]!); + const argsList = lowerer.lowerExpr(call.arguments.nodes[1]!); if (argsList.type.kind !== "dyn") return null; const loc = locOf(call); const savedCallee = lowerer.declareHiddenLocal("%applyFn", callee.type); const savedArgs = lowerer.declareHiddenLocal("%applyArgs", DYN); const stmts: IrStmt[] = [ { kind: "varDecl", localId: savedCallee.id, init: callee, loc }, - { kind: "exprStmt", expr: lowerer.lowerExpr(call.arguments[0]!), loc }, + { kind: "exprStmt", expr: lowerer.lowerExpr(call.arguments.nodes[0]!), loc }, { kind: "varDecl", localId: savedArgs.id, init: argsList, loc }, ]; const args = callee.type.params.map((param, index): IrExpr => { @@ -8749,7 +8784,7 @@ function lowerArrowFunctionApply( type: DYN, loc, }; - return lowerer.coerceInto(call.arguments[1]!, read, param); + return lowerer.coerceInto(call.arguments.nodes[1]!, read, param); }); const invoked: IrExpr = { kind: "callValue", @@ -8789,7 +8824,7 @@ function lowerObjLitGenericMethodCall( SP_BRAND_METHODS.has(access.expression.name.text) && lowerer.isStdlibMember(access.expression) ) { - const thisArg = call.arguments[0]; + const thisArg = call.arguments.nodes[0]; const thisT = thisArg ? lowerer.mapTypeOf(lowerer.typeOf(thisArg)) : { kind: "undefinedT" as const }; @@ -8883,7 +8918,7 @@ function lowerObjLitGenericMethodCall( if ( value?.type.kind === "func" && lowerer.dynConvertible(value.type) && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ) { const loc = locOf(call); return { @@ -8891,7 +8926,7 @@ function lowerObjLitGenericMethodCall( recv: { kind: "dynFrom", value, type: DYN, loc }, method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8919,7 +8954,7 @@ function lowerObjLitGenericMethodCall( // dispatches by name to the runtime object's member, which a generic // method answers with its erased instance; the result converts to this // call's instantiated return type. - if (onInterface && !call.arguments.some(ts.isSpreadElement)) { + if (onInterface && !call.arguments.nodes.some(ts.isSpreadElement)) { const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind === "dyn") { const loc = locOf(call); @@ -8928,7 +8963,7 @@ function lowerObjLitGenericMethodCall( recv: receiver, method: name, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8952,7 +8987,7 @@ function lowerObjLitGenericMethodCall( const info = objLitGenericFnInfoOf(lowerer, call, name, found); const instance = genericCallInstance(lowerer, call, info); const loc = locOf(call); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } @@ -9171,14 +9206,14 @@ export function lowerObjectMethodCall( if (callee?.type.kind === "func") { if ( ((callee.type.rest === true && callee.type.restAbi === undefined) || - call.arguments.length > callee.type.params.length) && + call.arguments.nodes.length > callee.type.params.length) && isJsSourceFile(call.getSourceFile()) && lowerer.dynConvertible(callee.type) ) { const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; const spread = lowerSpreadArgsCall(lowerer, call, boxed, loc); if (spread?.kind === "dynCall") return finish({ ...spread, ...receiver }); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return finish({ kind: "dynCall", callee: boxed, @@ -9193,10 +9228,10 @@ export function lowerObjectMethodCall( return finish({ kind: "callValue", callee, ...receiver, args, type: callee.type.ret, loc }); } if (callee?.type.kind === "dyn") { - if (call.arguments.some((a) => ts.isSpreadElement(a))) { + if (call.arguments.nodes.some((a) => ts.isSpreadElement(a))) { lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } - const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, DYN)); + const args = call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)); return finish({ kind: "dynCall", callee, @@ -9228,7 +9263,7 @@ export function lowerObjectMethodCall( type: DYN, loc, }; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -9241,7 +9276,7 @@ export function lowerObjectMethodCall( callee, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -9262,7 +9297,7 @@ export function lowerObjectMethodCall( const receiver = lowerReceiver(); const local = lowerer.declareHiddenLocal("%callbackReceiver", receiver.type); const value: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc }; - const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); const virtual = lowerer.overrideBelow(info, method); if (virtual) lowerer.noteVirtualEdge(info, method); else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); @@ -9286,7 +9321,7 @@ export function lowerObjectMethodCall( lowerer, value, method, - call.arguments, + call.arguments.nodes, fallback, loc, access.getText(), @@ -9318,7 +9353,7 @@ export function lowerObjectMethodCall( if (lowerer.overrideBelow(info, method)) lowerer.noteVirtualEdge(info, method); else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); const receiver = lowerReceiver(); - const args = lowerer.completeArgs(call.arguments, found.sig.params, locOf(call), call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, locOf(call), call); if (lowerer.overrideBelow(info, method)) { return reconcileOverloadReturn(lowerer, call, { kind: "virtualCall", @@ -9379,7 +9414,7 @@ function lowerUnionObjectMethodCall( type: DYN, loc, }; - const spread = call.arguments.some(ts.isSpreadElement) + const spread = call.arguments.nodes.some(ts.isSpreadElement) ? lowerSpreadArgsCall(lowerer, call, callee, loc) : null; if (spread && spread.kind !== "dynCall") @@ -9392,7 +9427,7 @@ function lowerUnionObjectMethodCall( callee, receiver, calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -9423,7 +9458,7 @@ function lowerUnionObjectMethodCall( lowerer.lowerExpr(access.expression), receiverT, ); - const args = lowerer.completeArgs(call.arguments, shapes, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, shapes, loc, call); const helper = unionObjectMethodHelper( lowerer, call, @@ -9473,7 +9508,7 @@ function lowerUnionObjectDynFieldCall( if (!info || info.fields.get(field)?.kind !== "dyn") return null; plans.push({ arm, info }); } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments in calls through 'unknown' values"); } const loc = locOf(call); @@ -9545,7 +9580,7 @@ function lowerUnionObjectDynFieldCall( lowerer.liftedFns.push({ name: helper, params, returnType: DYN, locals, body, loc }); } const callee: IrExpr = { kind: "call", callee: helper, args: [receiverRef], type: DYN, loc }; - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); return { kind: "seqExpr", stmts: [{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc }], diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 093e7eeef..770c74840 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -162,7 +162,7 @@ import { reflectedClassStaticMethodValue } from "./class-method-values.js"; export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { const body = member.body; - const last = body?.statements.at(-1); + const last = body?.statements?.nodes.at(-1); if (!body || !last || !ts.isReturnStatement(last)) return false; let valid = true; const visit = (node: ts.Node): void => { @@ -302,7 +302,7 @@ export function storedClassValueType( if ( (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) && isJsSourceFile(callee.getSourceFile()) && - !callee.typeParameters?.length && + !callee.typeParameters?.nodes.length && !lowerer.preparingClassFactories.has(callee) ) { const signature = lowerer.checker.getSignatureFromDeclaration(callee); @@ -371,7 +371,7 @@ export function storedClassValueType( if ( !factory || !isJsSourceFile(factory.getSourceFile()) || - factory.typeParameters?.length || + factory.typeParameters?.nodes.length || !ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory || !ts.isVariableDeclarationList(factory.parent.parent) || @@ -764,8 +764,8 @@ function symbolSlotReturnType( if (fnLike.type !== undefined || !fnLike.body) return null; if (!isJsSourceFile(fnLike.getSourceFile())) return null; if (fnLike.asteriskToken !== undefined) return null; - if (fnLike.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - const stmts = fnLike.body.statements; + if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + const stmts = fnLike.body.statements.nodes; const last = stmts[stmts.length - 1]; if (!last || !ts.isReturnStatement(last)) return null; const returns: ts.ReturnStatement[] = []; @@ -1092,7 +1092,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E while (ts.isParenthesizedExpression(node)) node = node.expression; if (ts.isCallExpression(node) && type.kind === "bytes") { let prototypeArgument = false; - for (const argument of node.arguments) + for (const argument of node.arguments.nodes) ts.walkPreorder(argument, (part) => { if ( !ts.isPropertyAccessExpression(part) || @@ -1119,7 +1119,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E owner = owner.parent; if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) { const className = owner.name.text; - for (const statement of owner.getSourceFile().statements) { + for (const statement of owner.getSourceFile().statements.nodes) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) @@ -1428,9 +1428,9 @@ export const EMITTER_API_MEMBERS: ReadonlySet = new Set([ /** The decorators of a class-like or member node (they live in * `modifiers` since TS 4.8). */ export function decoratorNodesOf(n: ts.Node): ts.Decorator[] { - return (((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []) as ts.Node[]).filter( - (m): m is ts.Decorator => m.kind === ts.SyntaxKind.Decorator, - ); + return ( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []) as ts.Node[] + ).filter((m): m is ts.Decorator => m.kind === ts.SyntaxKind.Decorator); } /** The AMBIENT name a decorator expression's evaluation throws on, or @@ -1488,13 +1488,13 @@ export function guaranteedDecorationThrow( if (v === "opaque") return null; if (v !== "effectFree") return v; } - const heritage = decl.heritageClauses?.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) - ?.types[0]; + const heritage = decl.heritageClauses?.nodes.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) + ?.types?.nodes[0]; if (heritage) { const h = stripParens(heritage.expression); if (h.kind !== ts.SyntaxKind.NullKeyword && !ts.isIdentifier(h)) return null; } - for (const member of decl.members) { + for (const member of decl.members.nodes) { for (const d of decoratorNodesOf(member)) { const v = decoratorVerdict(d); if (v === "opaque") return null; @@ -1531,9 +1531,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // A forward type reference can demand this class before collection // reaches its earlier base declaration. Collect that dependency first; // a later same-file base remains subject to the runtime order boundary. - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const reference = heritage && ts.isPropertyAccessExpression(heritage) ? heritage.name : heritage; const baseSymbol = @@ -1563,10 +1563,10 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // classStaticBlock13/28 miscompiles). const hasDeclTimeCode = (n: ts.Node): boolean => ts.isClassStaticBlockDeclaration(n) || - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).some( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); - if (symbol && (hasDeclTimeCode(decl) || decl.members.some(hasDeclTimeCode))) { + if (symbol && (hasDeclTimeCode(decl) || decl.members.nodes.some(hasDeclTimeCode))) { lowerer.reportDeferred(symbol); } // A poisoned BASE this class EXTENDS must report EAGERLY for the same @@ -1580,9 +1580,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) // reports. Resolution runs under the collect pass's guard (this is // collectProgram), so the lookup neither flushes nor fences. { - const baseIdent = decl.heritageClauses - ?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword) - ?.types.map((t) => t.expression) + const baseIdent = decl.heritageClauses?.nodes + .find((h) => h.token === ts.SyntaxKind.ExtendsKeyword) + ?.types?.nodes.map((t) => t.expression) .filter(ts.isIdentifier)[0]; const baseSym = baseIdent ? lowerer.resolveValueSymbol(baseIdent) : null; if (baseSym) lowerer.reportDeferred(baseSym); @@ -1597,7 +1597,7 @@ function inferredEmptyCollectionFieldType( ): IrType | null { if ( !ts.isNewExpression(initializer) || - (initializer.arguments?.length ?? 0) !== 0 || + (initializer.arguments?.nodes.length ?? 0) !== 0 || !ts.isIdentifier(initializer.expression) ) return null; @@ -1637,10 +1637,10 @@ function inferredEmptyCollectionFieldType( if ( collection === "Map" && node.expression.name.text === "set" && - node.arguments.length === 2 + node.arguments.nodes.length === 2 ) { - const key = mappedArgument(node.arguments[0]!); - const value = mappedArgument(node.arguments[1]!); + const key = mappedArgument(node.arguments.nodes[0]!); + const value = mappedArgument(node.arguments.nodes[1]!); if ( key !== null && value !== null && @@ -1659,9 +1659,9 @@ function inferredEmptyCollectionFieldType( (node.expression.name.text === "add" || node.expression.name.text === "has" || node.expression.name.text === "delete") && - node.arguments.length === 1 + node.arguments.nodes.length === 1 ) { - const element = mappedArgument(node.arguments[0]!); + const element = mappedArgument(node.arguments.nodes[0]!); if ( element !== null && isSupportedSetElem( @@ -1673,7 +1673,7 @@ function inferredEmptyCollectionFieldType( } return undefined; }; - for (const member of decl.members) { + for (const member of decl.members.nodes) { const body = ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member) || ts.isAccessor(member) ? member.body @@ -1745,7 +1745,7 @@ export function collectClassShapeInner( const classDecoratorNodes: ts.Decorator[] = []; { classDecoratorNodes.push(...decoratorNodesOf(decl)); - const decoratedMembers = decl.members.filter((m) => decoratorNodesOf(m).length > 0); + const decoratedMembers = decl.members.nodes.filter((m) => decoratorNodesOf(m).length > 0); if (classDecoratorNodes.length > 0 || decoratedMembers.length > 0) { // Node itself cannot execute decorator syntax in a JavaScript // source (V8 has not shipped the proposal; the type-stripping @@ -1811,7 +1811,7 @@ export function collectClassShapeInner( : ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ? "accessor decorators" : ts.isPropertyDeclaration(member) - ? member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword) + ? member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AccessorKeyword) ? "auto-accessor decorators" : "field decorators" : "member decorators"; @@ -1912,13 +1912,13 @@ export function collectClassShapeInner( "a generic base whose type arguments have no concrete layout", ); } - for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses ?? [])) { + for (const clause of inst || mixin || computedBase ? [] : (decl.heritageClauses?.nodes ?? [])) { // `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 // question, owned by the shape-coercion fences at those sites.) if (clause.token === ts.SyntaxKind.ImplementsKeyword) continue; - const t = clause.types[0]; + const t = clause.types.nodes[0]; if (familyMode && t) { const reference = ts.isPropertyAccessExpression(t.expression) ? t.expression.name @@ -1953,14 +1953,14 @@ export function collectClassShapeInner( globalError.def.name !== "%DOMException" && globalError.def.name !== "%AggregateError" ) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = globalError; continue; } const emitterBase = lowerer.builtinEmitterInfoOf(resolved); const streamBaseNs = builtinStreamInfoOf(lowerer, resolved); if (emitterBase || streamBaseNs) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = (emitterBase ?? streamBaseNs)!; continue; } @@ -1983,7 +1983,7 @@ export function collectClassShapeInner( `extending the namespace member '${t.expression.name.text}' (no class lowering)`, ); } - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = nsBase; continue; } @@ -2001,7 +2001,8 @@ export function collectClassShapeInner( if (!t.expression.questionDotToken) { const propBase = propertyAssignedClassInfoOf(lowerer, memberSym); if (propBase) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) + lowerer.unsupported("SC1090", t, "extending generic classes"); const baseDecl = propBase.decl; if ( baseDecl != null && @@ -2042,7 +2043,7 @@ export function collectClassShapeInner( // so its statics queue ahead of the derived class's — the // recursion order delivers exactly that). if (t && ts.isClassExpression(t.expression)) { - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); const baseExpr = lowerer.lowerClassExpressionInfo(t.expression); base = baseExpr; continue; @@ -2053,7 +2054,7 @@ export function collectClassShapeInner( // through the monomorphized chain (lower-mixins.ts). A call whose // callee is NOT a mixin function keeps the computed-expression // fence below. - if (t && ts.isCallExpression(t.expression) && !t.typeArguments) { + if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) { const mixinBase = lowerer.mixinCallClassInfoOf(t.expression); if (mixinBase) { base = mixinBase; @@ -2166,7 +2167,7 @@ export function collectClassShapeInner( base = instBase; continue; } - if (t.typeArguments) lowerer.unsupported("SC1090", t, "extending generic classes"); + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); base = named; if (!base) { if (adoptCallableBase(t.expression)) continue; @@ -2179,9 +2180,9 @@ export function collectClassShapeInner( } if (base?.localClass && !functionLocalClass(decl) && !computedBase && !factoryBaseExpression) { - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]; + )?.types?.nodes[0]; if (heritage) factoryBaseExpression = heritage.expression; else lowerer.unsupported("SC1090", decl, "extending a function-local class outside a function"); @@ -2256,13 +2257,15 @@ export function collectClassShapeInner( const checkedStatics = isJsSourceFile(decl.getSourceFile()) && ((base && hasRuntimeStatics(base)) || - decl.members.some( + decl.members.nodes.some( (member) => ts.isAccessor(member) && !ts.isPrivateIdentifier(member.name) && - member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), )) && - !decl.members.some( + !decl.members.nodes.some( (member) => member.name && ts.isPrivateIdentifier(member.name) && @@ -2301,12 +2304,12 @@ export function collectClassShapeInner( if (fnNode.asteriskToken) lowerer.unsupported("SC1071", member); const mName = (member.name as ts.Identifier | ts.PrivateIdentifier).text; const typeParams: ts.Symbol[] = []; - for (const tp of fnNode.typeParameters!) { + for (const tp of fnNode.typeParameters!.nodes) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", member, "this method form"); typeParams.push(sym); } - for (const param of fnNode.parameters) { + for (const param of fnNode.parameters.nodes) { if ( !ts.isIdentifier(param.name) && !ts.isObjectBindingPattern(param.name) && @@ -2323,7 +2326,7 @@ export function collectClassShapeInner( instances: new Map(), }); }; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && isJsSourceFile(decl.getSourceFile()) && @@ -2491,7 +2494,7 @@ export function collectClassShapeInner( if (errorRooted && memberName && classMemberNameOf(lowerer, memberName) === "cause") { if ( ts.isPropertyDeclaration(member) && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ) { // A TypeScript redeclaration is the JavaScript field: Node // defines the Error's own `cause` (undefined without an @@ -2586,12 +2589,12 @@ export function collectClassShapeInner( !inst && !mixin && ts.isMethodDeclaration(member) && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ) { const reason = emitOverrideShapeReason(lowerer, member); if (reason === null) { - const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters[0]!.name); - const restSym = lowerer.checker.getSymbolAtLocation(member.parameters[1]!.name); + const eventSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[0]!.name); + const restSym = lowerer.checker.getSymbolAtLocation(member.parameters.nodes[1]!.name); if (eventSym && restSym) { emitOverride = { decl: member, eventSym, restSym }; continue; @@ -2850,11 +2853,11 @@ export function collectClassShapeInner( // the ordinary undefined-armed union machinery. let type = lowerer.irTypeOf(member.name); if (isJsSourceFile(member.getSourceFile()) && !member.initializer) { - const constructor = decl.members.find( + const constructor = decl.members.nodes.find( (part): part is ts.ConstructorDeclaration => ts.isConstructorDeclaration(part) && !!part.body, ); - for (const statement of constructor?.body?.statements ?? []) { + for (const statement of constructor?.body?.statements?.nodes ?? []) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) || @@ -3008,8 +3011,8 @@ export function collectClassShapeInner( // FIRST (before every declared field, as undefined), and the // assignments run after super() and the field initializers, in // parameter order (see paramPropInitStmts). - for (const p of member.parameters) { - const isParamProp = p.modifiers?.some( + for (const p of member.parameters.nodes) { + const isParamProp = p.modifiers?.nodes.some( (m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || @@ -3020,7 +3023,7 @@ export function collectClassShapeInner( if (!isParamProp) { // Non-keyword modifiers (parameter decorators) are rejected // by tsc under standard decorators; defensive. - if (p.modifiers?.length) lowerer.unsupported("SC1090", p, "this parameter form"); + if (p.modifiers?.nodes.length) lowerer.unsupported("SC1090", p, "this parameter form"); continue; } // tsc rejects binding patterns (TS1187) and rest params @@ -3300,7 +3303,7 @@ export function collectClassShapeInner( // Defaults stay local to each declaration, so only inherited // required/optional slots participate in this refinement. for (let i = 0; i < shapes.length; i++) { - const parameter = member.parameters[i]; + const parameter = member.parameters.nodes[i]; const inherited = overridden.sig.params[i]; if ( parameter && @@ -3364,16 +3367,16 @@ export function collectClassShapeInner( const returnOnly = overridden.sig.params.length === shapes.length && overridden.sig.params.every((p, i) => typeEquals(p.type, shapes[i]!.type)) && - !member.parameters.some((parameter, index) => { + !member.parameters.nodes.some((parameter, index) => { if (!parameter.initializer) return false; - const baseMember = overridden.declarer.decl?.members.find( + const baseMember = overridden.declarer.decl?.members?.nodes.find( (candidate) => ts.isMethodDeclaration(candidate) && classMemberNameOf(lowerer, candidate.name) === mName, ); const inherited = baseMember && ts.isMethodDeclaration(baseMember) - ? baseMember.parameters[index]?.initializer + ? baseMember.parameters.nodes[index]?.initializer : undefined; if (!inherited) return false; const ownType = lowerer.mapTypeOf(lowerer.typeOf(parameter.initializer)); @@ -3407,7 +3410,7 @@ export function collectClassShapeInner( lowerer.unionReturnOverrides.add(member); } const asyncMember = - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; // Async methods dispatch STATICALLY (the body enters through its // fiber spawn wrapper; vtable slots hold raw implementations) — // an override chain touching an async method on either end would @@ -3532,7 +3535,7 @@ export function collectClassShapeInner( sig = { params: [], ret }; } else { // tsc rejects optional/default/rest setter params (TS1051-53). - sig = { params: lowerer.paramShapes(member.parameters), ret: VOID }; + sig = { params: lowerer.paramShapes(member.parameters.nodes), ret: VOID }; } if (lowerer.checkedVirtualJsAccessors.has(member)) { if (isGet) sig.ret = DYN; @@ -3604,7 +3607,7 @@ export function collectClassShapeInner( // slot (a nullable instance pointer) starts empty and whose reads check // it. Fields assigned before any exposure keep their plain reads. const derived = - decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? + decl.heritageClauses?.nodes.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ?? false; // Generic instances and mixins share layouts across declarations, so // they only report whether construction exposes the instance. @@ -3633,7 +3636,7 @@ export function collectClassShapeInner( const callee = call.expression; if (!ts.isPropertyAccessExpression(callee)) return null; const name = callee.name.text; - const declaration = decl.members.find( + const declaration = decl.members.nodes.find( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && member.body !== undefined && @@ -3665,7 +3668,7 @@ export function collectClassShapeInner( const deferredInitFields = new Set(base?.deferredInitFields ?? []); if (unguardedFields.length > 0) { const topAssigned = new Set(); - for (const stmt of ctor?.body?.statements ?? []) { + for (const stmt of ctor?.body?.statements?.nodes ?? []) { if ( ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && @@ -3852,7 +3855,7 @@ export function collectClassShapeInner( ) : null; if (ctor?.body) { - for (const stmt of ctor.body.statements) { + for (const stmt of ctor.body.statements.nodes) { const lhs = ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) && @@ -4090,10 +4093,10 @@ export function collectClassShapeInner( // fence, never a mis-typed array. const mixinForwarding = mixin !== undefined && ctor !== null && mixinForwardingCtor(lowerer, ctor); - if (mixin && ctor && !mixinForwarding && ctor.parameters.some((p) => p.dotDotDotToken)) { + if (mixin && ctor && !mixinForwarding && ctor.parameters.nodes.some((p) => p.dotDotDotToken)) { lowerer.unsupported( "SC1090", - ctor.parameters.find((p) => p.dotDotDotToken)!, + ctor.parameters.nodes.find((p) => p.dotDotDotToken)!, "mixin constructors whose rest parameter does anything but forward (`super(...args)` as the first statement is the compiled shape)", ); } @@ -4102,7 +4105,7 @@ export function collectClassShapeInner( // synthesized constructor forwards the same params to super). const ctorParams: ParamShape[] = ctor && !mixinForwarding - ? lowerer.paramShapes(ctor.parameters) + ? lowerer.paramShapes(ctor.parameters.nodes) : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && @@ -4126,13 +4129,13 @@ export function collectClassShapeInner( jsName: jsNameOverride ?? decl.name?.text ?? "", ...(base ? { base: base.def.name } : {}), jsLength: - ctor?.parameters.findIndex( + ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, ) === -1 - ? ctor.parameters.length + ? ctor.parameters.nodes.length : Math.max( 0, - ctor?.parameters.findIndex( + ctor?.parameters?.nodes.findIndex( (parameter) => parameter.initializer || parameter.dotDotDotToken, ) ?? 0, ), @@ -4216,7 +4219,7 @@ export function collectClassShapeInner( } if (familyMode) { const typeParams: ts.Symbol[] = []; - for (const tp of decl.typeParameters!) { + for (const tp of decl.typeParameters.nodes) { const sym = lowerer.checker.getSymbolAtLocation(tp.name); if (!sym) lowerer.unsupported("SC1090", tp, "this type parameter form"); typeParams.push(sym); @@ -4985,7 +4988,7 @@ function constructorMethodReads( const out: ts.Node[] = []; let owner: ts.ClassLikeDeclaration | undefined = decl; for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) { - for (const member of owner.members) { + for (const member of owner.members.nodes) { if ( (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && @@ -5424,14 +5427,17 @@ export function lowerClassExpressionInfo( } if (ts.isFunctionLike(p)) local = true; } - if (local && expr.members.some((member) => member.name && ts.isPrivateIdentifier(member.name))) { + if ( + local && + expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) + ) { lowerer.unsupported("SC1090", expr, "function-local classes with private brands"); } if ( local && - (expr.typeParameters?.length || + (expr.typeParameters?.nodes.length || decoratorNodesOf(expr).length || - expr.members.some( + expr.members.nodes.some( (member) => decoratorNodesOf(member).length || (!isJsSourceFile(expr.getSourceFile()) && @@ -5439,7 +5445,7 @@ export function lowerClassExpressionInfo( ts .getModifiers(member) ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || - (ts.isMethodDeclaration(member) && member.typeParameters?.length), + (ts.isMethodDeclaration(member) && member.typeParameters?.nodes.length), )) ) { lowerer.unsupported( @@ -5455,9 +5461,9 @@ export function lowerClassExpressionInfo( lowerer.fnStack.length > 0 && !lowerer.computedClassBases.has(lowerer.classNamer(expr)) ) { - const heritage = expr.heritageClauses?.find( + const heritage = expr.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if ( heritage && (ts.isCallExpression(heritage) || @@ -5489,9 +5495,9 @@ export function lowerClassExpressionInfo( }); info.localClass = { context: null, bodies: new Map(), captureSources: [], ready: false }; if (info.base?.localClass) { - const baseExpression = expr.heritageClauses?.find( + const baseExpression = expr.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if (baseExpression) info.localClass.baseExpression = baseExpression; } } @@ -5526,7 +5532,7 @@ export function lowerClassExpressionInfo( (ts.isVariableDeclaration(holder) && holder.initializer !== undefined && ts.isVariableDeclarationList(holder.parent) && - holder.parent.declarations.length === 1 && + holder.parent.declarations.nodes.length === 1 && ts.isVariableStatement(holder.parent.parent) && ts.isSourceFile(holder.parent.parent.parent)) || (ts.isBinaryExpression(holder) && @@ -5556,7 +5562,7 @@ export function lowerClassExpressionInfo( export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr { if ( decoratorNodesOf(expr).length > 0 || - expr.members.some((m) => decoratorNodesOf(m).length > 0) + expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) ) { if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) { const thrown = guaranteedDecorationThrow(lowerer, expr); @@ -5854,7 +5860,7 @@ function countAssignmentsTo(assign: ts.BinaryExpression): number { }; const target = canon(assign.left); let n = 0; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExpressionStatement(stmt)) continue; const e = stmt.expression; if ( @@ -5997,7 +6003,7 @@ function staticCallOn( recv: lowerer.lowerExprExpecting(access.expression, DYN), method: access.name.text, calleeName: access.name.text, - args: call.arguments.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), + args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc, }; @@ -6044,7 +6050,7 @@ function staticCallOn( call, staticMethodForReceiver(lowerer, access, gfound.info, info, throughValue), ); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (throughValue && staticShadowBelow(info, access.name.text)) { @@ -6061,7 +6067,7 @@ function staticCallOn( call, staticMethodForReceiver(lowerer, access, specialized.info, info, throughValue), ); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } if (found.field !== undefined) { @@ -6075,7 +6081,7 @@ function staticCallOn( loc, }; const params = found.field.type.params; - const args = call.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + const args = call.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); for (let i = args.length; i < params.length; i++) { const absent = omittedArgFor(lowerer, params[i]!, loc); if (!absent) { @@ -6113,14 +6119,14 @@ function staticCallOn( return { kind: "call", callee: instance.name, - args: lowerer.completeArgs(call.arguments, instance.params, loc, call), + args: lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call), type: instance.returnType, loc, }; } const fnName = `%${found.declarer.def.name}.static:${access.name.text}`; lowerer.noteEdge(fnName); - const args = lowerer.completeArgs(call.arguments, found.method.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.method.params, loc, call); return { kind: "call", callee: fnName, args, type: found.method.ret, loc }; } @@ -6165,15 +6171,15 @@ function directRuntimeStaticCall( } } if (!declarer || !member || !declarer.decl) return null; - if (member.asteriskToken || member.typeParameters) return null; - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)) + if (member.asteriskToken || member.typeParameters?.nodes) return null; + if (member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)) return null; if (hasLexicalSuper(member) || capturesEnclosingScope(lowerer, member, declarer.decl)) return null; if (memberAssigned(lowerer, member, name) || bodyReadsArguments(member)) return null; const implicit = - member.parameters.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true); - if (member.parameters.length > 0 && !implicit) return null; + member.parameters.nodes.length === 0 ? null : implicitAnyParamSymbolsOf(lowerer, member, true); + if (member.parameters.nodes.length > 0 && !implicit) return null; let calls = declarer.runtimeStaticCalls; if (!calls) { calls = new Map(); @@ -6203,7 +6209,7 @@ function directRuntimeStaticCall( lowered = { kind: "call", callee: instance.name, - args: lowerer.completeArgs(call.arguments, instance.params, locOf(call), call), + args: lowerer.completeArgs(call.arguments.nodes, instance.params, locOf(call), call), type: instance.returnType, loc: locOf(call), }; @@ -6456,11 +6462,11 @@ function abstractMemberSignature( lowerer: Lowerer, member: ts.MethodDeclaration, ): { shapes: ParamShape[]; ret: IrType } { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (!ts.isIdentifier(param.name)) lowerer.unsupported("SC1031", param); } return { - shapes: lowerer.paramShapes(member.parameters), + shapes: lowerer.paramShapes(member.parameters.nodes), ret: lowerer.declaredReturnType(member, member.name), }; } @@ -6595,7 +6601,7 @@ export function collectInheritedJsFieldWrites( ts.walkPreorder(file, (node) => { if ( (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) || - !node.heritageClauses?.length + !node.heritageClauses?.nodes.length ) return; const writes = new Map(); @@ -6628,8 +6634,10 @@ export function collectInheritedJsFieldWrites( } part.forEachChild(scan); }; - for (const member of node.members) { - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + for (const member of node.members.nodes) { + if ( + member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) continue; if (ts.isPropertyDeclaration(member) && member.initializer) { const name = ts.isPrivateIdentifier(member.name) @@ -6692,14 +6700,16 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files)); const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { - const accessors = node.members.filter( + const accessors = node.members.nodes.filter( (member): member is ts.GetAccessorDeclaration | ts.SetAccessorDeclaration => (ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member)) && ts.isIdentifier(member.name) && - !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), ); if ( - node.heritageClauses?.length && + node.heritageClauses?.nodes.length && accessors.length && isJsSourceFile(node.getSourceFile()) ) { @@ -6722,7 +6732,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou ) { lowerer.checkedVirtualJsAccessors.add(declaration); lowerer.checkedVirtualJsAccessors.add(member); - for (const twin of node.members) + for (const twin of node.members.nodes) if ( (ts.isGetAccessorDeclaration(twin) || ts.isSetAccessorDeclaration(twin)) && ts.isIdentifier(twin.name) && @@ -6737,13 +6747,15 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou }; if (instance) markAccessors(instance); } - const members = node.members.filter( + const members = node.members.nodes.filter( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && - !member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), ); - if (node.heritageClauses?.length && members.length > 0) { + if (node.heritageClauses?.nodes.length && members.length > 0) { const symbol = lowerer.typeOf(node).getSymbol(); const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; const seen = new Set(); @@ -6764,25 +6776,27 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou lowerer.virtualJsMethods.add(declaration); if ( isJsSourceFile(member.getSourceFile()) && - !member.typeParameters && + !member.typeParameters?.nodes && !member.asteriskToken && - !member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && + !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && !bodyReadsArguments(member) && - member.parameters.every((p) => !p.dotDotDotToken) + member.parameters.nodes.every((p) => !p.dotDotDotToken) ) { lowerer.virtualJsMethodArity.set( declaration, Math.max( lowerer.virtualJsMethodArity.get(declaration) ?? 0, - member.parameters.length, + member.parameters.nodes.length, ), ); const checked = - !declaration.typeParameters && + !declaration.typeParameters?.nodes && !declaration.asteriskToken && - !declaration.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && + !declaration.modifiers?.nodes.some( + (m) => m.kind === ts.SyntaxKind.AsyncKeyword, + ) && !bodyReadsArguments(declaration) && - [...member.parameters, ...declaration.parameters].every( + [...member.parameters.nodes, ...declaration.parameters.nodes].every( (p) => !p.dotDotDotToken, ); if (checked) { @@ -6840,7 +6854,8 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(method.getSourceFile()) && method.parameters.every(documentedParameter), + lowerer.checkedJsFile(method.getSourceFile()) && + method.parameters.nodes.every(documentedParameter), ); } @@ -6972,7 +6987,7 @@ export function lowerClassGenericMethodCall( loc, } : upcastTo(lowerer, receiver, declarer.def.name); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, @@ -7004,19 +7019,19 @@ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrE * form with an exact static story under monomorphization (the * instantiation adopts the base's ABI; see collectClassShapeInner). */ function mixinForwardingCtor(lowerer: Lowerer, ctor: ts.ConstructorDeclaration): boolean { - if (ctor.parameters.length !== 1 || !ctor.body) return false; - const p = ctor.parameters[0]!; + if (ctor.parameters.nodes.length !== 1 || !ctor.body) return false; + const p = ctor.parameters.nodes[0]!; if (!p.dotDotDotToken || !ts.isIdentifier(p.name)) return false; const paramName = p.name; const paramSym = lowerer.checker.getSymbolAtLocation(paramName); if (!paramSym) return false; - const first = ctor.body.statements[0]; + const first = ctor.body.statements.nodes[0]; if (!first || !ts.isExpressionStatement(first) || !ts.isCallExpression(first.expression)) return false; const call = first.expression; if (call.expression.kind !== ts.SyntaxKind.SuperKeyword) return false; - if (call.arguments.length !== 1) return false; - const a = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return false; + const a = call.arguments.nodes[0]!; if (!ts.isSpreadElement(a) || !ts.isIdentifier(a.expression)) return false; const spreadIdent = a.expression; if (lowerer.checker.getSymbolAtLocation(spreadIdent) !== paramSym) return false; @@ -7173,7 +7188,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { // The default-param prologue runs FIRST — before field initializers // and (in a derived class) before super(): JS evaluates parameter // defaults on entry, ahead of everything the body does. - const declared = lowerer.declareParams(info.ctor.parameters, info.ctorParams); + const declared = lowerer.declareParams(info.ctor.parameters.nodes, info.ctorParams); params.push(...declared.params); if (info.ctorParams.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); @@ -7188,7 +7203,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { // parameter property sees undefined). body.push(...lowerer.fieldInitStmts(info, thisLocal)); body.push(...paramPropInitStmts(lowerer, info, thisLocal)); - if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements)); + if (info.ctor.body) body.push(...lowerer.lowerStmts(info.ctor.body.statements.nodes)); } else if (info.ctor.body) { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } @@ -7241,7 +7256,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { * private, static and abstract members are not prototype string keys. */ function classPrototypeNames(decl: ts.ClassLikeDeclaration): string[] { const names: string[] = []; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( !ts.isMethodDeclaration(member) && !ts.isGetAccessorDeclaration(member) && @@ -7685,7 +7700,7 @@ function lowerClassMethodMemberInner( const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }]; // `this` is declared first, so method parameter DEFAULTS may use it // (JS allows this in method defaults; it is param 0 here). - const declared = lowerer.declareParams(fnLike.parameters, sig.params); + const declared = lowerer.declareParams(fnLike.parameters.nodes, sig.params); params.push(...declared.params); // A shorter JS override still implements every inherited ABI slot. // These tail parameters have no source binding or default prologue. @@ -7695,7 +7710,7 @@ function lowerClassMethodMemberInner( params.push({ localId: local.id, name, type: shape.type }); if (shape.mode === "arguments") fnCtx.argumentsLocal = local; } - const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements)]; + const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike)); const fn: IrFunction = { name: `%${className}.${mName}`, @@ -7730,7 +7745,7 @@ export function lowerStaticMethod( const entry = info.staticMethods?.get(name); if (!entry?.member.body) return null; const isAsync = - entry.member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + entry.member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = entry.member.asteriskToken !== undefined; if (isGenerator && (entry.ret.kind !== "generator" || (entry.ret.async === true) !== isAsync)) { lowerer.badType(entry.member.name, lowerer.typeOf(entry.member.name)); @@ -7757,13 +7772,13 @@ export function lowerStaticMethod( (keyword) => `'${keyword}' in static methods (it names the RECEIVER class — a dynamic value; reference the class by name instead)`, ); - const declared = lowerer.declareParams(entry.member.parameters, entry.params); + const declared = lowerer.declareParams(entry.member.parameters.nodes, entry.params); if (entry.params.at(-1)?.mode === "arguments") { const local = lowerer.declareHiddenLocal("%arguments", DYN); declared.params.push({ localId: local.id, name: "%arguments", type: DYN }); fnCtx.argumentsLocal = local; } - const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements)]; + const body = [...declared.prologue, ...lowerer.lowerStmts(entry.member.body.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(entry.member)); const fn: IrFunction = { name: `%${info.def.name}.static:${name}`, @@ -8008,7 +8023,7 @@ export function lowerDerivedCtorBody( }, ]; if (info.callableBase) { - if (superCall.arguments.some(ts.isSpreadElement)) + if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, @@ -8016,14 +8031,14 @@ export function lowerDerivedCtorBody( ); const pack: IrExpr = { kind: "dynArrLit", - elems: superCall.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + elems: superCall.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; statements.push(lowerer.superCallStmt(info, thisLocal, [pack], loc)); } else { const base = superBaseOf(info)!; - if (base.builtinEmitter && superCall.arguments.length > 0) + if (base.builtinEmitter && superCall.arguments.nodes.length > 0) lowerer.unsupported( "SC1090", superCall, @@ -8035,7 +8050,7 @@ export function lowerDerivedCtorBody( lowerer, info, base, - superCall.arguments, + superCall.arguments.nodes, thisLocal, loc, superCall, @@ -8047,10 +8062,10 @@ export function lowerDerivedCtorBody( !base.builtinError && !base.builtinEmitter && base.ctorParams.length === 0 && - superCall.arguments.some(ts.isSpreadElement); + superCall.arguments.nodes.some(ts.isSpreadElement); if (surplusSpread) { const evaluated = lowerer.completeArgs( - superCall.arguments, + superCall.arguments.nodes, [{ mode: "dynRest", type: DYN }], loc, superCall, @@ -8062,20 +8077,20 @@ export function lowerDerivedCtorBody( !base.builtinError && !base.builtinEmitter && !base.ctorParams.some((param) => param.mode === "rest" || param.mode === "dynRest") && - !superCall.arguments.some(ts.isSpreadElement) && - superCall.arguments.length > base.ctorParams.length; + !superCall.arguments.nodes.some(ts.isSpreadElement) && + superCall.arguments.nodes.length > base.ctorParams.length; let args = surplusSpread ? [] : forward !== undefined ? forward : base.builtinError - ? lowerer.errorConstructorArgs(superCall.arguments, loc, superCall) + ? lowerer.errorConstructorArgs(superCall.arguments.nodes, loc, superCall) : base.builtinEmitter ? [] : lowerer.completeArgs( surplus - ? superCall.arguments.slice(0, base.ctorParams.length) - : superCall.arguments, + ? superCall.arguments.nodes.slice(0, base.ctorParams.length) + : superCall.arguments.nodes, base.ctorParams, loc, superCall, @@ -8086,7 +8101,7 @@ export function lowerDerivedCtorBody( statements.push({ kind: "varDecl", localId: local.id, init: arg, loc }); return { kind: "varRef", localId: local.id, type: arg.type, loc }; }); - for (const argument of superCall.arguments.slice(base.ctorParams.length)) { + for (const argument of superCall.arguments.nodes.slice(base.ctorParams.length)) { statements.push({ kind: "exprStmt", expr: lowerer.lowerExpr(argument), @@ -8130,7 +8145,7 @@ export function lowerDerivedCtorBody( }; }; try { - const result = lowerer.lowerStmts(info.ctor!.body!.statements); + const result = lowerer.lowerStmts(info.ctor!.body!.statements.nodes); result.push({ kind: "if", cond: { @@ -8332,7 +8347,7 @@ export function lowerSuperMethodCall( const instance = genericCallInstance(lowerer, call, gfound.info); const loc = locOf(call); const thisRef: IrExpr = { kind: "varRef", localId: thisL.id, type: thisL.type, loc }; - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); return { kind: "call", callee: instance.name, @@ -8395,7 +8410,7 @@ export function lowerSuperMethodCall( callee, receiver: lowerer.coerceToExpected(thisRef, DYN), calleeName: access.getText(), - args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, }; @@ -8412,7 +8427,7 @@ export function lowerSuperMethodCall( }; } lowerer.noteEdge(`%${found.declarer.def.name}.${access.name.text}`); - const args = lowerer.completeArgs(call.arguments, found.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call); return { kind: "call", callee: `%${found.declarer.def.name}.${access.name.text}`, @@ -8675,25 +8690,25 @@ function lowerProgramClassNew( // zero arguments (the options bag fences, like the super() form). // Stream subclasses come first: their chain roots at the emitter // too, but the message should name the stream story. - if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments ?? []).length > 0) { + if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments?.nodes ?? []).length > 0) { lowerer.noLowering( `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`, - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "declare a constructor that passes an inline options object to super(...)", ); } - if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments ?? []).length > 0) { + if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } // A ctor-less chain into a builtin error base inherits both arguments; // each synthesized constructor forwards the same pair to its base. const args = lowerer.inheritsBuiltinErrorCtor(info) - ? lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr) - : lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); + ? lowerer.errorConstructorArgs(expr.arguments?.nodes ?? [], loc, expr) + : lowerer.completeArgs(expr.arguments?.nodes ?? [], info.ctorParams, loc, expr); for (let current: ClassInfo | null = info; current; current = current.base) { if (current.def.baseValueGlobal) return { @@ -8780,7 +8795,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { .declarationsOf(symbol) .find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); if (declaration && ts.isFunctionDeclaration(declaration) && declaration.body) { - const last = declaration.body.statements.at(-1); + const last = declaration.body.statements.nodes.at(-1); if ( last && ts.isReturnStatement(last) && @@ -8827,7 +8842,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { expr.expression.name.text === "Segmenter" && lowerer.isStdlibGlobal(expr.expression.expression, "Intl") ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const undefinedArg = (arg: ts.Expression): boolean => (lowerer.typeOf(arg).flags & ts.TypeFlags.Undefined) !== 0; if (args.length > 2 || (args[0] && !undefinedArg(args[0]))) { @@ -8843,7 +8858,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { !undefinedArg(options) && !( ts.isObjectLiteralExpression(options) && - options.properties.every( + options.properties.nodes.every( (property) => ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) && @@ -8894,10 +8909,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { : memberSym; const emitterInfo = lowerer.builtinEmitterInfoOf(resolved); if (emitterInfo) { - if ((expr.arguments ?? []).length > 0) { + if ((expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } @@ -8919,7 +8934,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const zlibModeNs = zlibStreamModeOf(lowerer, resolved); if (zlibModeNs !== null) { const what = `new zlib.${zlibClassName(zlibModeNs)}`; - return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibModeNs, what, expr, loc); + return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibModeNs, what, expr, loc); } // `new N.C(...)` / `new a.Point(...)` — construction through a // USER namespace qualifier (import= alias chains included): the @@ -9005,7 +9020,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; below(info); const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; - const args = lowerer.completeArgs(expr.arguments ?? [], info.ctorParams, loc, expr); + const args = lowerer.completeArgs( + expr.arguments?.nodes ?? [], + info.ctorParams, + loc, + expr, + ); return { kind: "newValue", callee, @@ -9046,7 +9066,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const identifier = ts.isIdentifier(expr.expression) ? expr.expression : expr.expression.name; const symbol = ts.isIdentifier(identifier) ? lowerer.resolveValueSymbol(identifier) : null; if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Array")) { - return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); + return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } if (isNativeProxyInitializer(lowerer, expr)) return lowerNativeProxy(lowerer, expr); // `new Error(msg?)` and its standard subclasses: the @@ -9061,7 +9081,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `cause` own-property record) / "Error" for absent, and the legacy // numeric code from the name table. if (errInfo && errInfo.def.name === "%DOMException") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new DOMException with ${args.length} arguments`, expr); } @@ -9100,7 +9120,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (errInfo && errInfo.def.name === "%AssertionError") { // new AssertionError(options): Node validates the options object and // generates the message from operator, actual and expected. - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new AssertionError with this argument list`, expr); } @@ -9113,7 +9133,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo && errInfo.def.name === "%SuppressedError") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new SuppressedError with this argument list`, expr); } @@ -9128,7 +9148,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo && errInfo.def.name === "%AggregateError") { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`new AggregateError with this argument list`, expr); } @@ -9142,7 +9162,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { }; } if (errInfo) { - const args = lowerer.errorConstructorArgs(expr.arguments ?? [], loc, expr); + const args = lowerer.errorConstructorArgs(expr.arguments?.nodes ?? [], loc, expr); return { kind: "libCall", fn: "error.newOptions", @@ -9156,10 +9176,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // (@types/node's captureRejections) has no lowering. const emitterInfo = lowerer.builtinEmitterInfoOf(symbol); if (emitterInfo) { - if ((expr.arguments ?? []).length > 0) { + if ((expr.arguments?.nodes ?? []).length > 0) { lowerer.unsupported( "SC1090", - expr.arguments![0]!, + expr.arguments!.nodes[0]!, "EventEmitter constructor options ('captureRejections')", ); } @@ -9180,7 +9200,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const zlibMode = zlibStreamModeOf(lowerer, symbol); if (zlibMode !== null) { const what = `new zlib.${zlibClassName(zlibMode)}`; - return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibMode, what, expr, loc); + return lowerZlibStreamNew(lowerer, expr.arguments?.nodes ?? [], zlibMode, what, expr, loc); } // `new URL(input)`: the WHATWG URL class (stdlib/@types provenance — // a user's own `class URL` resolves through classBySymbol below). @@ -9190,7 +9210,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // so bad input throws Node's catchable SyntaxError at construction. // Checked arguments retain native regexes and perform runtime coercion. if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr); } @@ -9270,7 +9290,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // for every getter and toISOString; identity and setters stay // fenced, so copying the scalar cannot create an observable lie. if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering( `new Date with ${args.length} arguments`, @@ -9324,7 +9344,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8, // Node's default. if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); } @@ -9397,22 +9417,22 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) { - const object = lowerObjectConstructor(lowerer, expr.arguments ?? [], loc); + const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc); if (object) return object; } if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { - return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []); + return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } // A const alias (`const M = Map`) constructs the same Map. if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) { - const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null; + const seedArg = (expr.arguments?.nodes.length ?? 0) === 1 ? expr.arguments!.nodes[0]! : null; const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression => ts.isArrayLiteralExpression(el) && - el.elements.length === 2 && - !el.elements.some(ts.isSpreadElement); + el.elements.nodes.length === 2 && + !el.elements.nodes.some(ts.isSpreadElement); const entriesLit = - seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.every(isPairLit) - ? seedArg.elements.filter(isPairLit) + seedArg && ts.isArrayLiteralExpression(seedArg) && seedArg.elements.nodes.every(isPairLit) + ? seedArg.elements.nodes.filter(isPairLit) : null; let tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); @@ -9429,8 +9449,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `satisfies` checks a view but retains the expression's own type. const contextualSeed = !ts.isSatisfiesExpression(parent) && - (expr.typeArguments?.length ?? 0) === 0 && - (entriesLit !== null || (expr.arguments?.length ?? 0) === 0); + (expr.typeArguments?.nodes.length ?? 0) === 0 && + (entriesLit !== null || (expr.arguments?.nodes.length ?? 0) === 0); // JavaScript's `new Map()` has no type-argument syntax: the no-arg // constructor overload pins Map whatever the JSDoc says // (`@type` on the declaration types the VARIABLE, not this @@ -9449,7 +9469,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const seeded = lowerMapSeedNew(lowerer, seedArg, mapped); if (seeded) return seeded; } - if ((expr.arguments?.length ?? 0) > 0 && !entriesLit) { + if ((expr.arguments?.nodes.length ?? 0) > 0 && !entriesLit) { lowerer.noLowering( "new Map(entries)", expr, @@ -9460,11 +9480,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (mapped?.kind === "map") { if (!entriesLit) return { kind: "mapNew", type: mapped, loc }; const seed = entriesLit.map((pair) => ({ - key: lowerer.lowerCollectionKey(pair.elements[0]!, mapped.key), + key: lowerer.lowerCollectionKey(pair.elements.nodes[0]!, mapped.key), value: mapped.value.kind === "dyn" - ? lowerer.lowerCollectionKey(pair.elements[1]!, mapped.value) - : lowerer.lowerExprExpecting(pair.elements[1]!, mapped.value), + ? lowerer.lowerCollectionKey(pair.elements.nodes[1]!, mapped.value) + : lowerer.lowerExprExpecting(pair.elements.nodes[1]!, mapped.value), })); return { kind: "mapNew", seed, type: mapped, loc }; } @@ -9479,7 +9499,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // keeps the compile fence. if ( isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) === 0 && + (expr.arguments?.nodes.length ?? 0) === 0 && targs.length > 0 && targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0) ) { @@ -9517,7 +9537,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && lowerer.isStdlibSymbol(symbol) ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.unsupported( "SC1090", @@ -9541,22 +9561,22 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { let mapped = lowerer.mapTypeOf(tsType); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "set" && fieldType?.kind === "set") mapped = fieldType; - if (mapped?.kind === "set" && (expr.arguments?.length ?? 0) === 1) { - const seeded = lowerSetSeedNew(lowerer, expr.arguments![0]!, mapped); + if (mapped?.kind === "set" && (expr.arguments?.nodes.length ?? 0) === 1) { + const seeded = lowerSetSeedNew(lowerer, expr.arguments!.nodes[0]!, mapped); if (seeded) return seeded; } if ( !mapped && isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) <= 1 && - !expr.arguments?.some(ts.isSpreadElement) + (expr.arguments?.nodes.length ?? 0) <= 1 && + !expr.arguments?.nodes.some(ts.isSpreadElement) ) { return { kind: "libCall", fn: "dyn.nativeSetNew", args: [ - expr.arguments?.[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + expr.arguments?.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), ], type: DYN, @@ -9571,12 +9591,14 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if ( !mapped && isJsSourceFile(expr.getSourceFile()) && - (expr.arguments?.length ?? 0) === 1 && - ts.isArrayLiteralExpression(expr.arguments![0]!) && - !(expr.arguments![0] as ts.ArrayLiteralExpression).elements.some(ts.isSpreadElement) + (expr.arguments?.nodes.length ?? 0) === 1 && + ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) && + !(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some( + ts.isSpreadElement, + ) ) { - const lit = expr.arguments![0] as ts.ArrayLiteralExpression; - const elems = lit.elements.map((el) => lowerer.lowerExpr(el)); + const lit = expr.arguments!.nodes[0] as ts.ArrayLiteralExpression; + const elems = lit.elements.nodes.map((el) => lowerer.lowerExpr(el)); const first = elems[0]; if ( first !== undefined && @@ -9588,7 +9610,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return { kind: "setNew", seed, type: setT, loc }; } } - if ((expr.arguments?.length ?? 0) > 0) { + if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new Set(values)", expr, @@ -9612,7 +9634,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // an f64 handle (type-mapper.ts), the Channel story. Construction options // ({ defaultValue, name }) have no lowering yet. if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) { - if ((expr.arguments?.length ?? 0) > 0) { + if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new AsyncLocalStorage(options)", expr, @@ -9625,7 +9647,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) { const type = lowerer.irTypeOf(expr); if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr)); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length !== 1) { lowerer.unsupported("SC1090", expr, "Promise construction without an executor"); } @@ -9697,12 +9719,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // constructing forms would owe the deprecation warning. if ( expr.expression.text === "Buffer" && - expr.arguments?.length === 2 && - !expr.arguments.some(ts.isSpreadElement) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind !== "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" + expr.arguments?.nodes.length === 2 && + !expr.arguments.nodes.some(ts.isSpreadElement) && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" && + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" ) { - const first = lowerer.lowerExpr(expr.arguments[0]!); + const first = lowerer.lowerExpr(expr.arguments.nodes[0]!); if ( first.kind === "unitLit" || first.type.kind === "dyn" || @@ -9714,7 +9736,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // throw only via the ctor body's later reads — it does NOT // observe it before throwing, so dropping it is exact for // effect-free operands; effectful ones keep the fence. - const enc = lowerer.lowerExpr(expr.arguments[1]!); + const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (enc.kind === "strLit" || isSafeToRepeat(enc)) { return { kind: "libCall", @@ -9778,7 +9800,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return bi?.module === "crypto" && bi.member === "X509Certificate"; })()); if (isX509) { - const args: readonly ts.Expression[] = expr.arguments ?? []; + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; if (args.length !== 1) { lowerer.noLowering( "X509Certificate with this argument shape", @@ -9918,7 +9940,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // time (EventEmitter's options have no lowering at all). if ( (info.builtinStream !== undefined || info.builtinEmitter) && - (expr.arguments?.length ?? 0) > 0 + (expr.arguments?.nodes.length ?? 0) > 0 ) { lowerer.unsupported( "SC1090", @@ -9927,7 +9949,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { ); } defineClassValueConstruct(lowerer, info); - const args = lowerer.completeArgs(expr.arguments ?? [], classValueParams(info), loc, expr); + const args = lowerer.completeArgs( + expr.arguments?.nodes ?? [], + classValueParams(info), + loc, + expr, + ); const built: IrExpr = { kind: "newValue", callee, @@ -10012,7 +10039,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { function lowerCheckedConstructor(lowerer: Lowerer, expr: ts.NewExpression, callee: IrExpr): IrExpr { const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { const packed = lowerer.completeArgs(args, [{ type: DYN, mode: "dynRest" }], loc, expr)[0]!; return checkedClassConstructionPacked(lowerer, callee, packed, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index 71b9183d5..f811d13b6 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -50,12 +50,12 @@ function describeComptimeValue(v: unknown): string { export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // 1. Inline function literal only (tsc guarantees the arity/type; the // literal-ness is our requirement — source extraction needs the source). - let cb: ts.Expression | undefined = expr.arguments[0]; + let cb: ts.Expression | undefined = expr.arguments.nodes[0]; while (cb && ts.isParenthesizedExpression(cb)) cb = cb.expression; if (!cb || !(ts.isArrowFunction(cb) || ts.isFunctionExpression(cb))) { lowerer.unsupported( "SC1090", - expr.arguments[0] ?? expr, + expr.arguments.nodes[0] ?? expr, "comptime arguments other than inline function literals " + "(write the callback directly in the call: comptime(() => ...))", ); @@ -63,7 +63,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr // 2. Async/generator callbacks have no bakeable result. if ( (ts.isFunctionExpression(cb) && cb.asteriskToken) || - cb.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) + cb.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) { lowerer.unsupported("SC1090", cb, "async or generator comptime callbacks"); } diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index 20f5129b4..a7fac4f51 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -67,11 +67,11 @@ type OptionRole = "stdout" | "stderr" | "ignoreErrors"; export function lowerConsoleNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (!isConsoleCtor(lowerer, expr.expression)) return null; const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const entries: { role: OptionRole; node: ts.Expression | undefined }[] = []; const first = args[0]; if (first && ts.isObjectLiteralExpression(first)) { - for (const prop of first.properties) { + for (const prop of first.properties.nodes) { let name: string | null = null; let value: ts.Expression | null = null; if ( @@ -349,11 +349,15 @@ export function lowerConsoleOutput( // formatter IS util.format — route through the format lowering and // print its one string. Specifier-free first strings keep the // plain space-joined path below (identical output, cheaper). - const first = expr.arguments[0]; - if (expr.arguments.length > 1 && isStringLiteralLike(first) && hasFormatSpecifier(first.text)) { + const first = expr.arguments.nodes[0]; + if ( + expr.arguments.nodes.length > 1 && + isStringLiteralLike(first) && + hasFormatSpecifier(first.text) + ) { return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc); } - const args = expr.arguments.map((a) => + const args = expr.arguments.nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc), ); return consoleWrite(stdout, args, loc); @@ -479,9 +483,9 @@ function lowerConsoleTimeLog( loc: SrcLoc, ): IrExpr { const stmts: IrStmt[] = []; - const raw = rawLabel(lowerer, member, expr.arguments[0]); + const raw = rawLabel(lowerer, member, expr.arguments.nodes[0]); const rawRef = raw === null ? null : hoist(lowerer, stmts, "%consoleLabel", raw, loc); - const data = expr.arguments.slice(1); + const data = expr.arguments.nodes.slice(1); const dataRefs: { node: ts.Expression; value: IrExpr }[] = []; if (member === "timeLog") { for (const node of data) { @@ -546,7 +550,7 @@ function lowerConsoleTimeLog( * so a format string still formats. */ function lowerConsoleAssert(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const stmts: IrStmt[] = []; - const [valueNode, ...args] = expr.arguments; + const [valueNode, ...args] = expr.arguments.nodes; const ok = valueNode === undefined ? boolLit(false, loc) @@ -635,7 +639,7 @@ function lowerConsoleAssert(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcL * in the runtime (cli_table's layout over util.inspect cells). */ function lowerConsoleTable(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const stmts: IrStmt[] = []; - const [dataNode, propsNode, ...rest] = expr.arguments; + const [dataNode, propsNode, ...rest] = expr.arguments.nodes; if (dataNode === undefined) return consoleWrite(true, [strLit("undefined", loc)], loc); let data = lowerer.lowerExpr(dataNode); let props = @@ -707,7 +711,7 @@ export function lowerConsoleMethodCall( member: ConsoleMethodMember, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; switch (member) { case "dirxml": return lowerConsoleOutput(lowerer, expr, true, "console.dirxml", loc); diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 06c83da44..54f9d158b 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -132,7 +132,7 @@ export function lowerDgramDnsModuleCall( ): IrExpr | null { if (bi.module === "dns") return lowerDnsModuleCall(lowerer, expr, bi, loc); if (bi.module !== "dgram") return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createSocket") { if (args.length !== 1) { lowerer.noLowering( @@ -172,7 +172,7 @@ export function lowerDgramDnsModuleCall( } let sawType = false; let reuseAddr: IrExpr | null = null; - for (const prop of arg.properties) { + for (const prop of arg.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( "createSocket options with computed keys, spreads, or shorthand entries", @@ -289,7 +289,7 @@ function lowerDnsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const dyn = (node: ts.Expression): IrExpr => lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN); // A callback whose second parameter is typed (string, LookupAddress[]) @@ -382,7 +382,7 @@ export function lowerDgramMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "bind" || name === "connect") { requireStatementPosition(lowerer, call, `socket.${name}(...)`); // bind(port[, address][, cb]) / connect(port[, address][, cb]) — the @@ -675,7 +675,7 @@ export function lowerPromisifiedDnsCall( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`the promisified dns.${member} with spread arguments`, expr); } diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index d9a30028e..03e22e684 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -96,7 +96,7 @@ export function lowerEnumAccess( // member (merged declarations scan in declaration order). if (ts.isStringLiteralLike(key)) { for (const d of decls) { - for (const member of d.members) { + for (const member of d.members.nodes) { if (memberNameText(member) === key.text) return lowerEnumMemberRead(lowerer, expr, member); } } @@ -191,7 +191,7 @@ function lowerEnumReverseRead( } let name: string | null = null; for (const d of decls) { - for (const member of d.members) { + for (const member of d.members.nodes) { const v = lowerer.checker.getConstantValue(member); if (typeof v === "number" && v === idx) name = memberNameText(member); } @@ -242,7 +242,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { * the declaration fences on that member. */ export function lowerEnumDeclaration(lowerer: Lowerer, stmt: ts.EnumDeclaration): null { if (isAmbientEnumDecl(stmt)) return null; - for (const member of stmt.members) { + for (const member of stmt.members.nodes) { if (lowerer.checker.getConstantValue(member) === undefined) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index e73bbd4ce..b8297fe91 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -288,7 +288,7 @@ function emitterEvents(lowerer: Lowerer): Map { const member = node.expression.name.text; const isEmit = member === "emit"; if (!isEmit && !REGISTER_MEMBERS.has(member)) return; - const arg0 = node.arguments[0]; + const arg0 = node.arguments.nodes[0]; if (!arg0) return; let recvT: IrType | null = null; let nameT: ts.Type | null = null; @@ -327,12 +327,13 @@ function emitterEvents(lowerer: Lowerer): Map { if (isEmit) { mergeEmit( name, - node.arguments.slice(1).map((a) => lowerer.mapTypeOf(lowerer.typeOf(a))), + node.arguments.nodes.slice(1).map((a) => lowerer.mapTypeOf(lowerer.typeOf(a))), isJsSourceFile(sf), ); - } else if (node.arguments[1]) { - const cbCt = lowerer.typeOf(node.arguments[1]); - const cbT = lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments[1], cbCt); + } else if (node.arguments.nodes[1]) { + const cbCt = lowerer.typeOf(node.arguments.nodes[1]); + const cbT = + lowerer.mapTypeOf(cbCt) ?? dynFallbackType(lowerer, node.arguments.nodes[1], cbCt); if ( cbT?.kind !== "dyn" && (cbT?.kind !== "func" || cbT.params.some((p) => p.kind !== "dyn")) @@ -503,7 +504,7 @@ function staticStringSuffixOf( while (ts.isParenthesizedExpression(expression)) expression = expression.expression; if (ts.isStringLiteralLike(expression)) return expression.text; if (ts.isTemplateExpression(expression)) - return expression.templateSpans.at(-1)?.literal.text ?? ""; + return expression.templateSpans.nodes.at(-1)?.literal.text ?? ""; if (ts.isIdentifier(expression)) { const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; @@ -935,8 +936,11 @@ function lowerListenerArg( ): IrExpr { // Unannotated non-empty parameter lists have no static types (checker // `any`; dyn in JS sources) — say so before the blanket type fence speaks. - if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.parameters.length > 0) { - for (const p of node.parameters) { + if ( + (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && + node.parameters.nodes.length > 0 + ) { + for (const p of node.parameters.nodes) { if (p.type === undefined && ts.isIdentifier(p.name)) { const mapped = (() => { try { @@ -1009,7 +1013,7 @@ export function lowerEmitterMethodCall( ): IrExpr | null { const member = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const lowerReceiver = (): IrExpr => { if (superRecv) return superRecv.thisRef; const receiver = lowerer.lowerExpr(access.expression); @@ -1643,22 +1647,22 @@ export function emitOverrideShapeReason( if ((member as { questionToken?: ts.Node }).questionToken !== undefined) return "optional method declarations have no lowering"; if (member.typeParameters !== undefined) return "the compiled shape declares no type parameters"; - for (const m of member.modifiers ?? []) { + for (const m of member.modifiers?.nodes ?? []) { if (m.kind === ts.SyntaxKind.AsyncKeyword) return "async methods answer a Promise, not emit's boolean"; if (m.kind === ts.SyntaxKind.AbstractKeyword) return "abstract declarations have no body to lower"; if (m.kind === ts.SyntaxKind.Decorator) return "decorated methods have no lowering"; } - if (member.parameters.length !== 2) return "it must declare exactly (event, ...args)"; - const p0 = member.parameters[0]!; - const p1 = member.parameters[1]!; + if (member.parameters.nodes.length !== 2) return "it must declare exactly (event, ...args)"; + const p0 = member.parameters.nodes[0]!; + const p1 = member.parameters.nodes[1]!; if ( !ts.isIdentifier(p0.name) || p0.dotDotDotToken !== undefined || p0.questionToken !== undefined || p0.initializer !== undefined || - (p0.modifiers?.length ?? 0) > 0 + (p0.modifiers?.nodes.length ?? 0) > 0 ) { return "the event parameter must be a plain identifier"; } @@ -1666,7 +1670,7 @@ export function emitOverrideShapeReason( !p1.dotDotDotToken || !ts.isIdentifier(p1.name) || p1.initializer !== undefined || - (p1.modifiers?.length ?? 0) > 0 + (p1.modifiers?.nodes.length ?? 0) > 0 ) { return "the second parameter must be a plain rest parameter (...args)"; } @@ -1732,7 +1736,11 @@ function isEmitForwardSpread( const sp = id.parent; if (!ts.isSpreadElement(sp) || sp.expression !== id) return false; const call = sp.parent; - if (!ts.isCallExpression(call) || call.arguments.length !== 2 || call.arguments[1] !== sp) + if ( + !ts.isCallExpression(call) || + call.arguments.nodes.length !== 2 || + call.arguments.nodes[1] !== sp + ) return false; const callee = call.expression; if ( @@ -1742,7 +1750,7 @@ function isEmitForwardSpread( ) { return false; } - const a0 = call.arguments[0]!; + const a0 = call.arguments.nodes[0]!; if (!ts.isIdentifier(a0)) return false; try { if (lowerer.checker.getSymbolAtLocation(a0) !== eventSym) return false; @@ -1893,7 +1901,7 @@ export function lowerEmitOverrideSpec(lowerer: Lowerer, req: EmitSpecRequest): I params.push({ localId: p.id, name: `%ee${i}`, type: t }); return p; }); - const eventDecl = ov.decl.parameters[0]!; + const eventDecl = ov.decl.parameters.nodes[0]!; const eventLocal = lowerer.declareLocal( eventDecl.name, ts.isIdentifier(eventDecl.name) ? eventDecl.name.text : "event", @@ -1911,7 +1919,7 @@ export function lowerEmitOverrideSpec(lowerer: Lowerer, req: EmitSpecRequest): I restSym: ov.restSym, tupleParams, }; - const body = [...prologue, ...lowerer.lowerStmts(ov.decl.body.statements)]; + const body = [...prologue, ...lowerer.lowerStmts(ov.decl.body.statements.nodes)]; return { name: `%${className}.emit:${event}`, params, @@ -1940,9 +1948,9 @@ export function emitSpecSuperForward( access: ts.PropertyAccessExpression, ): IrExpr | null { const ctx = lowerer.emitSpecCtx; - if (!ctx || access.name.text !== "emit" || call.arguments.length !== 2) return null; - const a0 = call.arguments[0]!; - const a1 = call.arguments[1]!; + if (!ctx || access.name.text !== "emit" || call.arguments.nodes.length !== 2) return null; + const a0 = call.arguments.nodes[0]!; + const a1 = call.arguments.nodes[1]!; if (!ts.isIdentifier(a0) || !ts.isSpreadElement(a1) || !ts.isIdentifier(a1.expression)) return null; if (lowerer.checker.getSymbolAtLocation(a0) !== ctx.eventSym) return null; @@ -2192,7 +2200,7 @@ function eventsOnceBoxer( * array. EventTarget and stream receivers, computed names and options are * refused. */ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering("events.once without an emitter and an event name", call); } @@ -2302,7 +2310,7 @@ export function lowerEventsEmitterHelper( member: string, loc: SrcLoc, ): IrExpr | null { - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement)) { lowerer.noLowering(`events.${member} with spread arguments`, call); } diff --git a/packages/compiler/src/frontend/lowering/lower-event-target.ts b/packages/compiler/src/frontend/lowering/lower-event-target.ts index e6230009d..79bd59968 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-target.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-target.ts @@ -274,13 +274,13 @@ export function lowerEventTargetMethodCall( ): IrExpr { const member = access.name.text; const loc = locOf(call); - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with spread arguments`, call); } const target = eventTargetRootOf(info) === "target"; if (target) noteEventListeners(lowerer, loc); const { stmts, ref } = singleReceiver(lowerer, receiverExpr, loc); - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => target && index === 1 && member !== "dispatchEvent" ? lowerer.lowerLiveWebValue(arg) : lowerer.lowerExprExpecting(arg, DYN), diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 218cc61f6..5e4c43021 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -247,7 +247,7 @@ function nsOwnedMember( ) { if (ts.isModuleDeclaration(p)) { return !( - p.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || + p.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || p.getSourceFile().isDeclarationFile ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d46226384..09407f71f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -539,7 +539,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const pieces: IrExpr[] = []; const headRaw = templateRawTextOf(t.head); if (headRaw !== "") pieces.push({ kind: "strLit", value: headRaw, type: STRING, loc }); - for (const span of t.templateSpans) { + for (const span of t.templateSpans.nodes) { pieces.push( lowerer.caughtToString(span.expression) ?? lowerer.ensureString(lowerer.lowerExpr(span.expression), span.expression), @@ -2226,7 +2226,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { fn = ts.isSourceFile(fn) ? undefined : fn.parent; } if (fn !== undefined) { - const fixedArity = (fn as ts.FunctionLikeDeclaration).parameters.every( + const fixedArity = (fn as ts.FunctionLikeDeclaration).parameters.nodes.every( (p) => p.questionToken === undefined && p.initializer === undefined && @@ -2235,7 +2235,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (fixedArity) { return { kind: "numLit", - value: (fn as ts.FunctionLikeDeclaration).parameters.length, + value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.length, type: F64, loc, }; @@ -2994,11 +2994,11 @@ export function lowerTernary( if ( ts.isCallExpression(c) && ts.isPropertyAccessExpression(c.expression) && - c.arguments.length === 1 && - ts.isIdentifier(c.arguments[0]!) && + c.arguments.nodes.length === 1 && + ts.isIdentifier(c.arguments.nodes[0]!) && lowerer.stdlibGlobalMember(c.expression, "Array") === "isArray" ) { - const argIdent = c.arguments[0] as ts.Identifier; + const argIdent = c.arguments.nodes[0] as ts.Identifier; const sym = lowerer.resolveValueSymbol(argIdent); const t = lowerer.checker.getTypeAtLocation(argIdent); const constituents = t.isUnionType() ? ts.constituentTypes(t) : []; @@ -3072,7 +3072,7 @@ export function lowerTernary( }; const emptyUntypedArrayArm = (e: ts.Expression): boolean => { const lit = armLiteral(e); - if (!lit || lit.elements.length !== 0) return false; + if (!lit || lit.elements.nodes.length !== 0) return false; const t = lowerer.checker.getContextualType(lit) ?? lowerer.typeOf(lit); if (!lowerer.mapTypeOf(t)) return true; // never[] MAPS (the f64 representation for the uninhabited) but @@ -4057,7 +4057,7 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): const call = arg.parent; if ( !ts.isCallExpression(call) || - call.arguments[0] !== arg || + call.arguments.nodes[0] !== arg || !ts.isPropertyAccessExpression(call.expression) ) return false; @@ -4111,7 +4111,7 @@ function assertedElementKeepsUndefined(lowerer: Lowerer, node: ts.NonNullExpress lowerer.checker.isArrayType(lowerer.typeOf(receiver)); if ( ts.isCallExpression(parent) && - parent.arguments.includes(outer) && + parent.arguments.nodes.includes(outer) && ts.isPropertyAccessExpression(parent.expression) && (parent.expression.name.text === "push" || parent.expression.name.text === "unshift") && isArray(parent.expression.expression) @@ -5855,11 +5855,11 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean return stored?.kind === "dyn" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Object) !== 0; } if (ts.isArrayLiteralExpression(expr)) - return expr.elements.some( + return expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), ); if (ts.isObjectLiteralExpression(expr)) - return expr.properties.some( + return expr.properties.nodes.some( (property) => (ts.isPropertyAssignment(property) && jsHoldsDynObject(lowerer, property.initializer)) || (ts.isShorthandPropertyAssignment(property) && @@ -5955,7 +5955,7 @@ export function lowerArrayLiteral( // no ambiguity: build the literal as that arm and let the slot's // coercion wrap it. Two array arms would need tsc's own best-fit // choice — none arises in practice; the fence stands. - if (ctxUnion && expr.elements.length === 0) { + if (ctxUnion && expr.elements.nodes.length === 0) { const def = lowerer.unions.get(ctxUnion.unionId); const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; if (arrayArms.length === 1) mapped = arrayArms[0]!; @@ -5967,11 +5967,11 @@ export function lowerArrayLiteral( // arity-matching tuple (the option-table `default: [{ value: [] }]` // shape): build AS that arm, exactly the empty-literal rule above; // the slot's coercion wraps it. - if (ctxUnion && expr.elements.length > 0) { + if (ctxUnion && expr.elements.nodes.length > 0) { // A unit-only-element array (`(null | undefined)[]`) cannot hold // non-unit elements — neither as the literal's own mapping nor as // a competing union arm. - const nonUnitElems = expr.elements.some( + const nonUnitElems = expr.elements.nodes.some( (el) => !ts.isOmittedExpression(el) && el.kind !== ts.SyntaxKind.NullKeyword && @@ -5990,7 +5990,7 @@ export function lowerArrayLiteral( (a.kind === "array" && !(nonUnitElems && lowerer.unitOnlyElem(a.elem))) || (a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple && - lowerer.shapes.get(a.shapeId)!.fields.length === expr.elements.length), + lowerer.shapes.get(a.shapeId)!.fields.length === expr.elements.nodes.length), ); if (arms.length === 1) mapped = arms[0]!; } @@ -6002,7 +6002,7 @@ export function lowerArrayLiteral( !expected && mapped?.kind === "array" && mapped.elem.kind === "string" && - expr.elements.some((element) => isDateJsonSource(lowerer, element)) + expr.elements.nodes.some((element) => isDateJsonSource(lowerer, element)) ) mapped = { kind: "array", elem: dateJsonType(lowerer) }; // An array literal inside an npm-static implicit-any instance can @@ -6018,12 +6018,12 @@ export function lowerArrayLiteral( (mapped === null || mapped.kind === "dyn" || (mapped.kind === "array" && mapped.elem.kind === "dyn")) && - expr.elements.length > 0 && - expr.elements.every( + expr.elements.nodes.length > 0 && + expr.elements.nodes.every( (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { - const elements = expr.elements.map((element) => tryLowerExpression(lowerer, element)); + const elements = expr.elements.nodes.map((element) => tryLowerExpression(lowerer, element)); const first = elements[0]; if ( first !== undefined && @@ -6047,7 +6047,7 @@ export function lowerArrayLiteral( if ( !expected && isJsSourceFile(expr.getSourceFile()) && - expr.elements.some( + expr.elements.nodes.some( (element) => ts.isSpreadElement(element) && tryLowerExpression(lowerer, element.expression)?.type.kind === "dyn", @@ -6061,7 +6061,7 @@ export function lowerArrayLiteral( !expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind !== "dyn" && - expr.elements.some( + expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), ) ) @@ -6071,7 +6071,7 @@ export function lowerArrayLiteral( // mapType's own `[] as const` rule. if ( mapped === null && - expr.elements.length === 0 && + expr.elements.nodes.length === 0 && (tsType.flags & ts.TypeFlags.Any) !== 0 && underConstAssertion(expr) ) { @@ -6084,7 +6084,7 @@ export function lowerArrayLiteral( isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array" && mapped.elem.kind === "union" && - expr.elements.some(ts.isObjectLiteralExpression) + expr.elements.nodes.some(ts.isObjectLiteralExpression) ) mapped = DYN; @@ -6096,11 +6096,11 @@ export function lowerArrayLiteral( if (mapped?.kind === "record") { const shape = lowerer.shapes.get(mapped.shapeId); if (shape?.tuple) { - const spread = expr.elements.find(ts.isSpreadElement); + const spread = expr.elements.nodes.find(ts.isSpreadElement); if (spread) { return lowerTupleSpreadLiteral(lowerer, expr, mapped, shape); } - if (expr.elements.length !== shape.fields.length) { + if (expr.elements.nodes.length !== shape.fields.length) { // tsc padded an UNDER-LENGTH literal against an optional-element // tuple context (`options || []` with `options?: [string?, // number?]` — the instantiated type spells every position, the @@ -6108,7 +6108,7 @@ export function lowerArrayLiteral( lowerer.badType(expr, tsType); } const byName = new Map(shape.fields.map((f) => [f.name, f.type])); - const fields = expr.elements.map((el, i) => { + const fields = expr.elements.nodes.map((el, i) => { const fieldType = byName.get(String(i)); if (!fieldType) lowerer.badType(el, lowerer.typeOf(el)); return { name: String(i), value: lowerer.lowerExprExpecting(el, fieldType) }; @@ -6132,14 +6132,14 @@ export function lowerArrayLiteral( mapped?.kind === "dyn" || lowerer.checkerAnyArray(expr) ) { - if (expr.elements.some(ts.isSpreadElement)) { + if (expr.elements.nodes.some(ts.isSpreadElement)) { // Native collection drains return vectors of retained dyn // elements. Reuse the ordinary spread builder, then expose the // resulting array through the established unknown[] representation. const vector = lowerArrayLiteral(lowerer, expr, arrayOf(DYN) as IrType & { kind: "array" }); return lowerer.coerceInto(expr, vector, DYN); } - const elems = expr.elements.map((el): IrExpr => { + const elems = expr.elements.nodes.map((el): IrExpr => { if (ts.isSpreadElement(el)) { lowerer.unsupported( "SC1090", @@ -6167,7 +6167,7 @@ export function lowerArrayLiteral( // build that layout directly. An explicit destination keeps its own. if (!expected && type.elem.kind === "record") { let variant: IrType | null = null; - for (const candidate of expr.elements) { + for (const candidate of expr.elements.nodes) { let x: ts.Expression = candidate; while (ts.isParenthesizedExpression(x)) x = x.expression; if (!ts.isObjectLiteralExpression(x)) continue; @@ -6189,8 +6189,8 @@ export function lowerArrayLiteral( const holes = new Set(); const runtimeOptionalElements = new Set(); let needsStateBuild = false; - for (let i = 0; i < expr.elements.length; i++) { - const candidate = expr.elements[i]!; + for (let i = 0; i < expr.elements.nodes.length; i++) { + const candidate = expr.elements.nodes[i]!; if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate)) continue; const probe = tryLowerExpression(lowerer, candidate); if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) { @@ -6198,7 +6198,7 @@ export function lowerArrayLiteral( needsStateBuild = true; } } - const elems: IrExpr[] = expr.elements.map((el, i): IrExpr => { + const elems: IrExpr[] = expr.elements.nodes.map((el, i): IrExpr => { if (ts.isSpreadElement(el)) { // `[...xs, b]`: xs must be an array of the literal's own element // type — its elements copy in at construction (a fresh array, @@ -6443,7 +6443,7 @@ function lowerTupleSpreadLiteral( stmts.push({ kind: "varDecl", localId: temp.id, init: coerced, loc: at }); fields.push({ name, value: varRef(temp.id, expected, at) }); }; - for (const element of expr.elements) { + for (const element of expr.elements.nodes) { if (!ts.isSpreadElement(element)) { const expected = byName.get(String(fields.length)); if (!expected) lowerer.badType(element, lowerer.typeOf(element)); @@ -9277,7 +9277,7 @@ export function stringWrapperToString(lowerer: Lowerer, node: ts.Expression): Ir if (!ts.isNewExpression(e) || !ts.isIdentifier(e.expression)) return null; const symbol = lowerer.resolveValueSymbol(e.expression); if (symbol?.name !== "String" || !lowerer.isStdlibSymbol(symbol)) return null; - const args = e.arguments ?? []; + const args = e.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; if (args.length === 0) return { kind: "strLit", value: "", type: STRING, loc: locOf(e) }; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, args[0]!); @@ -9300,7 +9300,7 @@ export function lowerTemplate(lowerer: Lowerer, expr: ts.TemplateExpression): Ir if (expr.head.text !== "") { pieces.push({ kind: "strLit", value: expr.head.text, type: STRING, loc }); } - for (const span of expr.templateSpans) { + for (const span of expr.templateSpans.nodes) { pieces.push( stringWrapperToString(lowerer, span.expression) ?? lowerer.caughtToString(span.expression) ?? @@ -9346,7 +9346,7 @@ export function lowerAsExpression( ts.isAsExpression(expr) && isConstAssertionTypeNode(expr.type) && ts.isArrayLiteralExpression(expr.expression) && - expr.expression.elements.length === 0 + expr.expression.elements.nodes.length === 0 ) { return { kind: "arrayLit", @@ -12512,7 +12512,7 @@ function lowerReplacedExportsRead( return null; } let replaced = false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs) continue; if (cjs.kind === "table" && cjsExportDiscardReason(stmt) === null) replaced = true; @@ -12599,7 +12599,7 @@ function lowerCjsExportTableThisMember( ) { return null; } - const sibling = n.parent.properties.find( + const sibling = n.parent.properties.nodes.find( (p): p is ts.GetAccessorDeclaration => ts.isGetAccessorDeclaration(p) && ts.isIdentifier(p.name) && p.name.text === expr.name.text, ); @@ -13556,7 +13556,7 @@ function lowerPrivateIn( let staticBrand = false; for (let n: ts.Node | undefined = priv.parent; n; n = n.parent) { if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) { - const owner = n.members.find((m) => { + const owner = n.members.nodes.find((m) => { const name = (m as { name?: ts.PropertyName }).name; return name !== undefined && ts.isPrivateIdentifier(name) && name.text === pname; }); @@ -14411,11 +14411,11 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null expr.expression.text === "RegExp" && lowerer.isStdlibGlobal(expr.expression, "RegExp") && expr.arguments !== undefined && - expr.arguments.length >= 1 && - (ts.isStringLiteral(expr.arguments[0]!) || - ts.isNoSubstitutionTemplateLiteral(expr.arguments[0]!)) + expr.arguments.nodes.length >= 1 && + (ts.isStringLiteral(expr.arguments.nodes[0]!) || + ts.isNoSubstitutionTemplateLiteral(expr.arguments.nodes[0]!)) ) { - return (expr.arguments[0] as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text; + return (expr.arguments.nodes[0] as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text; } if (ts.isIdentifier(expr)) { const init = constInitializerOf(lowerer, expr); @@ -14489,7 +14489,7 @@ function staticRegexFlagsOf(lowerer: Lowerer, node: ts.Expression): string | nul ts.isIdentifier(expr.expression) && lowerer.isStdlibGlobal(expr.expression, "RegExp") ) { - const flags = expr.arguments?.[1]; + const flags = expr.arguments?.nodes[1]; if (!flags) return ""; if (ts.isStringLiteral(flags) || ts.isNoSubstitutionTemplateLiteral(flags)) return flags.text; } @@ -14515,17 +14515,17 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) { const name = expr.expression.name.text; const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression)); - if (name === "exec" && expr.arguments.length === 1 && recvT?.kind === "regex") { + if (name === "exec" && expr.arguments.nodes.length === 1 && recvT?.kind === "regex") { return expr.expression.expression; } if ( name === "match" && - expr.arguments.length === 1 && + expr.arguments.nodes.length === 1 && (recvT?.kind === "string" || (recvT !== null && recvT !== undefined && nullableStringType(lowerer, recvT))) && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "regex" ) { - return expr.arguments[0]!; + return expr.arguments.nodes[0]!; } } // A matchAll ROW by element access: rows[i] with rows tracing to the @@ -14592,7 +14592,7 @@ function matchAllCallbackRegexOf( param: ts.ParameterDeclaration, ): ts.Expression | null { const fn = param.parent; - if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters[0] !== param) + if (!(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.parameters.nodes[0] !== param) return null; // A reassigned parameter can hold a row of another regex. if (ts.isIdentifier(param.name)) { @@ -14606,17 +14606,17 @@ function matchAllCallbackRegexOf( const method = call.expression.name.text; if ( MATCH_ROW_CALLBACK_METHODS.has(method) && - call.arguments[0] === site && + call.arguments.nodes[0] === site && lowerer.isStdlibMember(call.expression) ) return matchAllRegexOf(lowerer, call.expression.expression); if ( method === "from" && - call.arguments[1] === site && + call.arguments.nodes[1] === site && lowerer.isStdlibGlobal(call.expression.expression, "Array") && - call.arguments[0] !== undefined + call.arguments.nodes[0] !== undefined ) - return matchAllRegexOf(lowerer, call.arguments[0]); + return matchAllRegexOf(lowerer, call.arguments.nodes[0]); return null; } @@ -14645,15 +14645,15 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "matchAll" && - expr.arguments.length === 1 && + expr.arguments.nodes.length === 1 && lowerer.mapTypeOf(lowerer.typeOf(expr.expression.expression))?.kind === "string" && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind === "regex" ) { - return expr.arguments[0]!; + return expr.arguments.nodes[0]!; } // The eager-spread spelling: `[...s.matchAll(re)]`. - if (ts.isArrayLiteralExpression(expr) && expr.elements.length === 1) { - const only = expr.elements[0]!; + if (ts.isArrayLiteralExpression(expr) && expr.elements.nodes.length === 1) { + const only = expr.elements.nodes[0]!; if (ts.isSpreadElement(only)) return matchAllRegexOf(lowerer, only.expression); } if ( @@ -15818,10 +15818,10 @@ export function uniqueSymbolKeyOf( if (!ts.isIdentifier(init.expression) || init.expression.text !== "Symbol") return null; if (!lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(init.expression))) return null; const arg = - init.arguments.length === 0 + init.arguments.nodes.length === 0 ? null - : init.arguments.length === 1 - ? init.arguments[0]! + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! : undefined; if (arg === undefined) return null; if (arg !== null && !ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) @@ -16356,7 +16356,7 @@ export function lowerPromiseRejectCall( if (call.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Promise") !== "reject") return null; const loc = locOf(call); - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( "Promise.reject with this argument count", call, @@ -16369,7 +16369,7 @@ export function lowerPromiseRejectCall( // returning `T | Promise`) names its one promise arm. const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null; const type = maybePromise ? maybePromise.promise : contextual; - const reasonNode = call.arguments[0]!; + const reasonNode = call.arguments.nodes[0]!; // A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode // an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree // value IS the rejection payload (the thrown-dyn representation, so @@ -16434,18 +16434,18 @@ export function lowerPromiseAllTupleCall( ): IrExpr | null { if (call.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Promise") !== "all") return null; - const argNode = call.arguments.length === 1 ? call.arguments[0]! : null; + const argNode = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null; if ( !argNode || !ts.isArrayLiteralExpression(argNode) || - argNode.elements.some(ts.isSpreadElement) || - argNode.elements.length === 0 + argNode.elements.nodes.some(ts.isSpreadElement) || + argNode.elements.nodes.length === 0 ) { return null; } // The claim test runs on checker types only (no side effects): every // element the same representable promise type. - const mapped = argNode.elements.map((el) => lowerer.mapTypeOf(lowerer.typeOf(el))); + const mapped = argNode.elements.nodes.map((el) => lowerer.mapTypeOf(lowerer.typeOf(el))); const first = mapped[0]; if (first?.kind !== "promise") return null; if (!mapped.every((m) => m?.kind === "promise" && typeEquals(m.inner, first.inner))) { @@ -16454,7 +16454,7 @@ export function lowerPromiseAllTupleCall( const loc = locOf(call); const inner = first.inner; const entryT: IrType = { kind: "promise", inner }; - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, entryT)); + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, entryT)); const entries: IrExpr = { kind: "arrayLit", elems, type: arrayOf(entryT), loc }; const type: IrType = inner.kind === "void" diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index b6d0a9863..7cc448994 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -171,10 +171,10 @@ export function lowerLambda( // A generic lambda used as a value has no per-call instantiation: it // lowers once with its type parameters bound to `unknown`, the erased // instance JavaScript itself runs. - if (node.typeParameters?.length && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes.length && !lowerer.erasingLambdas.has(node)) { const previous = lowerer.typeParamBindings; const bindings = new Map(previous ?? []); - for (const parameter of node.typeParameters) { + for (const parameter of node.typeParameters.nodes) { const symbol = lowerer.checker.getSymbolAtLocation(parameter.name); if (symbol) bindings.set(symbol, DYN); } @@ -253,7 +253,7 @@ export function lowerLambda( // the lifted body returns the inner T (a `return v` fulfills with v). const isAsync = !ts.isAccessor(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = node.asteriskToken !== undefined; if (isAsync && !isGenerator && funcType.ret.kind !== "promise") lowerer.badType(node, lowerer.typeOf(node)); @@ -303,11 +303,11 @@ export function lowerLambda( const receiver = (checkedReceiver || isJsSourceFile(node.getSourceFile()) || - node.parameters.some(isThisParameter)) && + node.parameters.nodes.some(isThisParameter)) && !ts.isArrowFunction(node) ? declareCallReceiver(lowerer, node, isGenerator) : null; - const { params, prologue } = lowerer.declareParams(node.parameters, shapes); + const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -323,7 +323,7 @@ export function lowerLambda( let body: IrStmt[]; if (ts.isBlock(node.body!)) { - body = lowerer.lowerStmts(node.body!.statements); + body = lowerer.lowerStmts(node.body!.statements.nodes); } else { // Bare-expression arrow body: `x => e` is `x => { return e; }` // (or an expression statement when the signature returns void — or @@ -494,10 +494,10 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.push(ctx); try { const receiver = - decl.parameters.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; - const { params, prologue } = lowerer.declareParams(decl.parameters, sig.params); + const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -506,7 +506,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I params, prologue, locOf(decl), - sig.params.length > decl.parameters.length && + sig.params.length > decl.parameters.nodes.length && (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); @@ -518,7 +518,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I "function declarations whose block body the frontend cannot locate", ); } - const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements)]; + const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl)); const fn: IrFunction = { name: sig.name, diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index a9b467a6a..6bf414f29 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -112,7 +112,7 @@ export function lowerGenMethodCall( const name = access.name.text; if (name !== "next" && name !== "return" && name !== "throw") return null; if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "generator") return null; - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.unsupported("SC1090", call, "spread arguments"); } // Iterator helpers and the built-in iterators have no throw method; an @@ -131,7 +131,7 @@ export function lowerGenMethodCall( const genT = gen.type; const recT = resultRecordOf(lowerer, genT); const resultT: IrType = genT.async ? { kind: "promise", inner: recT } : recT; - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; let arg: IrExpr | null = null; if (name === "next") { if (argNode === undefined) { @@ -379,7 +379,7 @@ export function generatorLoopBinding( lowerer.unsupported("SC1090", stmt.initializer, "for-of assignment to this target"); } const list = stmt.initializer; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[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 diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 37233bcdc..be7f21c1c 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1608,7 +1608,7 @@ function parseInspectOptions( "the options must be an object literal ({ depth, colors: false, ... }) so the knobs resolve at compile time", ); } - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( `${surface} options in this form`, @@ -1789,14 +1789,14 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo /** util.inspect(value[, options]) — the spoke's inspect entry. */ function lowerInspectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length < 1 || expr.arguments.length > 2) { + if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `util.inspect with ${expr.arguments.length} arguments`, + `util.inspect with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are inspect(value) and inspect(value, options)", ); } - return lowerInspectValue(lowerer, expr.arguments[0]!, expr.arguments[1], loc, false); + return lowerInspectValue(lowerer, expr.arguments.nodes[0]!, expr.arguments.nodes[1], loc, false); } /** `inspect(value, options)` over one value node: the shared body of @@ -2047,10 +2047,10 @@ export function lowerFormatCall( argNodesOverride?: readonly ts.Expression[], literalPrefix = "", ): IrExpr { - const argNodes = [...(argNodesOverride ?? expr.arguments)]; + const argNodes = [...(argNodesOverride ?? expr.arguments.nodes)]; if (withOptions) { const opts = argNodes.shift(); - if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.length > 0) { + if (!opts || !ts.isObjectLiteralExpression(opts) || opts.properties.nodes.length > 0) { lowerer.noLowering( "util.formatWithOptions with a non-empty options literal", opts ?? expr, @@ -2394,21 +2394,21 @@ export function lowerUtilModuleCall( switch (bi.member) { case "styleText": case "isDeepStrictEqual": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${bi.member} with spread arguments`, expr); const stmts: IrStmt[] = []; const args: IrExpr[] = []; // Save all inputs before evaluating surplus arguments: comparison may // read mutable objects or throw, so it must happen after every argument. for (let i = 0; i < 3; i++) { - const input: IrExpr = expr.arguments[i] - ? lowerer.lowerExprExpecting(expr.arguments[i]!, DYN) + const input: IrExpr = expr.arguments.nodes[i] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilInput", DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); args.push(varRef(saved.id, DYN, loc)); } - for (const arg of expr.arguments.slice(3)) + for (const arg of expr.arguments.nodes.slice(3)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = bi.member === "styleText" ? STRING : BOOL; return { @@ -2426,10 +2426,10 @@ export function lowerUtilModuleCall( case "debug": return lowerUtilCallableCall(lowerer, expr, bi.member, loc); case "getSystemErrorMap": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("util.getSystemErrorMap with spread arguments", expr); const result = lowerSystemErrorMap(lowerer, loc); - const stmts: IrStmt[] = expr.arguments.map((arg) => ({ + const stmts: IrStmt[] = expr.arguments.nodes.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc, @@ -2440,24 +2440,24 @@ export function lowerUtilModuleCall( case "getSystemErrorMessage": case "stripVTControlCharacters": case "toUSVString": { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${bi.member} with spread arguments`, expr); - const input = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : { kind: "dynFrom" as const, value: { kind: "unitLit" as const, unit: "undefined" as const, type: UNDEFINED_T, loc }, type: DYN, loc, }; - if (expr.arguments.length <= 1) + if (expr.arguments.nodes.length <= 1) return { kind: "libCall", fn: `util.${bi.member}`, args: [input], type: STRING, loc }; const saved = lowerer.declareHiddenLocal("%utilInput", DYN); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: saved.id, init: input, loc }, - ...expr.arguments + ...expr.arguments.nodes .slice(1) .map((arg): IrStmt => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc })), ], @@ -2481,11 +2481,11 @@ export function lowerUtilModuleCall( case "parseArgs": return lowerParseArgsCall(lowerer, expr, loc); case "parseEnv": { - if (expr.arguments.length > 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("util.parseEnv with spread or extra arguments", expr); } - const input: IrExpr = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const input: IrExpr = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); const parsed: IrExpr = { kind: "libCall", @@ -2526,7 +2526,7 @@ function lowerUtilCallableCall( member: string, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`util.${member} with spread arguments`, expr); const fn = member === "inherits" @@ -2540,7 +2540,7 @@ function lowerUtilCallableCall( // Program classes keep static prototypes: there is no function object // whose prototype chain the runtime could relink or wrap. const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0; - for (const arg of expr.arguments.slice(0, classArgs)) { + for (const arg of expr.arguments.nodes.slice(0, classArgs)) { if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval") lowerer.noLowering( `util.${member} over a class constructor`, @@ -2559,14 +2559,14 @@ function lowerUtilCallableCall( const stmts: IrStmt[] = []; const args: IrExpr[] = []; for (let i = 0; i < arity; i++) { - const input: IrExpr = expr.arguments[i] - ? lowerer.lowerExprExpecting(expr.arguments[i]!, DYN) + const input: IrExpr = expr.arguments.nodes[i] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN) : dynUndefinedExpr(loc); const saved = lowerer.declareHiddenLocal("%utilInput", DYN); stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc }); args.push(varRef(saved.id, DYN, loc)); } - for (const arg of expr.arguments.slice(arity)) + for (const arg of expr.arguments.nodes.slice(arity)) stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc }); const type = fn === "util.inherits" ? VOID : DYN; const result: IrExpr = { kind: "libCall", fn, args, type, loc }; @@ -2601,16 +2601,16 @@ function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression) * variants and a null-prototype values object); statically typed member * reads leave that tree through the ordinary dyn checks. */ function lowerParseArgsCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 1 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 1 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `util.parseArgs with ${expr.arguments.length} arguments`, + `util.parseArgs with ${expr.arguments.nodes.length} arguments`, expr, "the lowered form is parseArgs() or parseArgs(config)", ); } let config: IrExpr; - const configNode = expr.arguments[0]; + const configNode = expr.arguments.nodes[0]; if (!configNode) { config = { kind: "dynObjLit", fields: [], type: DYN, loc }; } else { @@ -2684,7 +2684,7 @@ function lowerGetCallSitesCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 0) return null; + if (expr.arguments.nodes.length !== 0) return null; if (!isJsSourceFile(expr.getSourceFile())) return null; const frame = (): IrExpr => { const field = (name: string, value: IrExpr): { key: IrExpr; value: IrExpr } => ({ diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index af5a1ab46..5677b2812 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -116,8 +116,8 @@ function mixinFnShapeInner( ): MixinFnShape | null { if (fn.asteriskToken) return null; if (ts.getModifiers(fn)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; - if (fn.parameters.length !== 1) return null; - const p = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const p = fn.parameters.nodes[0]!; if (!ts.isIdentifier(p.name) || p.dotDotDotToken || p.questionToken || p.initializer) return null; const paramSym = lowerer.checker.getSymbolAtLocation(p.name); if (!paramSym || !fn.body) return null; @@ -131,7 +131,7 @@ function mixinFnShapeInner( const e = unparen(fn.body); if (ts.isClassExpression(e)) classNode = e; } else { - const stmts = fn.body.statements; + const stmts = fn.body.statements.nodes; if (stmts.length === 1 && ts.isReturnStatement(stmts[0]!) && stmts[0]!.expression) { const e = unparen(stmts[0]!.expression!); if (ts.isClassExpression(e)) classNode = e; @@ -153,17 +153,17 @@ function mixinFnShapeInner( // A generic class would mint a FAMILY per call; decorators are // declaration-time calls with per-evaluation semantics — both // disqualify (decorator-composed mixins are a named non-goal). - if (classNode.typeParameters) return null; - const mods = (classNode as { modifiers?: readonly ts.Node[] }).modifiers ?? []; + if (classNode.typeParameters?.nodes) return null; + const mods = (classNode as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []; if (mods.some((m) => m.kind === ts.SyntaxKind.Decorator)) return null; // Heritage: exactly `extends `. - const clauses = classNode.heritageClauses ?? []; + const clauses = classNode.heritageClauses?.nodes ?? []; if (clauses.length !== 1 || clauses[0]!.token !== ts.SyntaxKind.ExtendsKeyword) return null; - const types = clauses[0]!.types; + const types = clauses[0]!.types.nodes; if (types.length !== 1) return null; const t = types[0]!; - if (t.typeArguments || !ts.isIdentifier(t.expression)) return null; + if (t.typeArguments?.nodes || !ts.isIdentifier(t.expression)) return null; if (lowerer.checker.getSymbolAtLocation(t.expression) !== paramSym) return null; // The parameter's flow must be fully traceable: the extends clause is @@ -191,7 +191,7 @@ function mixinFnShapeInner( const refSym = lowerer.checker.getSymbolAtLocation(p.type.typeName); if ( refSym && - fn.typeParameters?.some((tp) => lowerer.checker.getSymbolAtLocation(tp.name) === refSym) + fn.typeParameters?.nodes.some((tp) => lowerer.checker.getSymbolAtLocation(tp.name) === refSym) ) { paramTypeParam = refSym; } @@ -415,7 +415,7 @@ function instantiateMixinCall( "mixin calls whose base chain re-enters their own instantiation (a cyclic extends)", ); } - if (call.arguments.length !== 1 || ts.isSpreadElement(call.arguments[0]!)) { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.unsupported( "SC1090", call, @@ -449,7 +449,7 @@ function instantiateMixinCall( } } - const base = mixinBaseClassOf(lowerer, call.arguments[0]!); + const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( call.getSourceFile(), `%mx${call.getStart()}.${shape.classNode.name?.text ?? ""}`, @@ -611,7 +611,8 @@ function pinnedMixinCallPosition(call: ts.CallExpression): boolean { n = p, p = p.parent ) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if (ts.isVariableDeclaration(p) && p.initializer === n) { return ts.isVariableDeclarationList(p.parent) && (p.parent.flags & ts.NodeFlags.Const) !== 0; } @@ -629,12 +630,13 @@ function staticsEvalStatementOf(call: ts.CallExpression): ts.Statement | null { let n: ts.Node = call; for (let p: ts.Node | undefined = n.parent; p !== undefined; n = p, p = p.parent) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if ( ts.isVariableDeclaration(p) && p.initializer === n && ts.isVariableDeclarationList(p.parent) && - p.parent.declarations.length === 1 && + p.parent.declarations.nodes.length === 1 && ts.isVariableStatement(p.parent.parent) && ts.isSourceFile(p.parent.parent.parent) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 2022d8232..4f446bb89 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -657,7 +657,7 @@ export function lowerMockFn( const fields = mockContextFields(lowerer, ctxType!)!; const { fn } = fields; requireUndefinedResultArm(lowerer, fields, call, what); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -717,7 +717,7 @@ export function lowerMockMethod( tracker: MockTracker, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -844,8 +844,8 @@ export function lowerMockTrackerCall( member: "restoreAll" | "reset", tracker: MockTracker, ): IrExpr { - if (call.arguments.length > 0) { - lowerer.noLowering(`MockTracker.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 0) { + lowerer.noLowering(`MockTracker.${member} with ${call.arguments.nodes.length} arguments`, call); } const fn = tracker ? member === "reset" @@ -893,7 +893,7 @@ export function lowerMockTimersCall( sym: ts.Symbol | null, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const optionalNumber = (fallback: number): IrExpr[] => { if (args.length > 1) lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call); @@ -935,9 +935,9 @@ export function lowerMockTimersCall( * (1 = `now` given, 2 = it is a Date). */ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { const loc = locOf(call); - const options = call.arguments[0]; - if (call.arguments.length > 1) { - lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call); + const options = call.arguments.nodes[0]; + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`${what}.enable with ${call.arguments.nodes.length} arguments`, call); } if (options === undefined) { return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)]; @@ -953,7 +953,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: let listed = false; let now: IrExpr = numLit(0, loc); let flags = 0; - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { // `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand. const init = ts.isPropertyAssignment(prop) ? prop.initializer @@ -974,7 +974,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: ); } listed = true; - for (const el of list.elements) { + for (const el of list.elements.nodes) { if (!ts.isStringLiteralLike(el)) { lowerer.noLowering( `${what}.enable with a non-literal API name`, @@ -1046,7 +1046,7 @@ export function lowerMockContextCall( const { fn, calls, once, onceEntry } = mockContextFields(lowerer, ctxType)!; const loc = locOf(call); const member = access.name.text; - const args = call.arguments; + const args = call.arguments.nodes; const arity = (min: number, max: number = min) => { if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) lowerer.noLowering(`MockFunctionContext.${member} with ${args.length} arguments`, call); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 128652bd2..bc963c9ec 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -243,7 +243,7 @@ export function splitFiles(lowerer: Lowerer): FileParts[] { const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; return files.map((sf) => { const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt); // Namespaces: ambient/type-only ones are zero-runtime and skip; @@ -293,10 +293,10 @@ function dynamicProgramImportsOf(program: ts.Program, sf: ts.SourceFile): ts.Sou if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments[0] !== undefined && - ts.isStringLiteralLike(node.arguments[0]) + node.arguments.nodes[0] !== undefined && + ts.isStringLiteralLike(node.arguments.nodes[0]) ) { - const dep = dynamicImportProgramTargetOf(program, sf, node.arguments[0].text); + const dep = dynamicImportProgramTargetOf(program, sf, node.arguments.nodes[0].text); if (dep && !seen.has(dep)) { seen.add(dep); out.push(dep); @@ -367,9 +367,9 @@ 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( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; if (heritage && ts.isCallExpression(heritage)) computed.add(decl); } // Descendants need the computed ancestor's completed layout before @@ -385,9 +385,9 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) for (const decl of fp.classDecls) { if (computed.has(decl)) continue; - const heritage = decl.heritageClauses?.find( + const heritage = decl.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const base = heritage && ts.isIdentifier(heritage) ? lowerer.resolveValueSymbol(heritage) : undefined; if (!base || !computedSymbols.has(base)) continue; @@ -440,9 +440,9 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { const previousSink = lowerer.diagSink; const deferred: import("../../diagnostics/diagnostic.js").ScrDiagnostic[] = []; lowerer.diagSink = deferred; - const expression = decl.heritageClauses!.find( + const expression = decl.heritageClauses!.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )!.types[0]!.expression; + )!.types.nodes[0]!.expression; const context = newFnCtx(false, null, null, DYN); lowerer.fnStack.push(context); try { @@ -515,7 +515,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { const entryPackageFile = entryPackageFilePredicate(lowerer.entry.fileName); for (const fp of parts) { - for (const stmt of fp.sf.statements) { + for (const stmt of fp.sf.statements.nodes) { // NAMED re-exports from npm packages (`export { isUrl } from // "url-or-path"`): use the same package bindings as direct imports. const reexport = @@ -525,7 +525,7 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { ts.isStringLiteral(stmt.moduleSpecifier) && stmt.exportClause !== undefined && ts.isNamedExports(stmt.exportClause) && - stmt.exportClause.elements.some((e) => !e.isTypeOnly) && + stmt.exportClause.elements.nodes.some((e) => !e.isTypeOnly) && !stmt.moduleSpecifier.text.startsWith("#") ? stmt.exportClause : null; @@ -546,7 +546,7 @@ export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { clause?.namedBindings && ts.isNamedImports(clause.namedBindings) && !clause.name && - clause.namedBindings.elements.every((e) => e.isTypeOnly) + clause.namedBindings.elements.nodes.every((e) => e.isTypeOnly) ) { continue; } @@ -633,11 +633,11 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && - node.arguments.length >= 1 && - node.arguments[0] !== undefined && - ts.isStringLiteralLike(node.arguments[0]) + node.arguments.nodes.length >= 1 && + node.arguments.nodes[0] !== undefined && + ts.isStringLiteralLike(node.arguments.nodes[0]) ) { - const spec = node.arguments[0].text; + const spec = node.arguments.nodes[0].text; if (lowerer.externalTypes.has(spec)) { return undefined; } @@ -648,7 +648,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { // module has no runtime namespace object — the per-site fence // says so. Only non-declaration source files count; a sibling // .d.mts typing shipped JS is the EMBED case, not this one. - const modSym = lowerer.checker.getSymbolAtLocation(node.arguments[0]); + const modSym = lowerer.checker.getSymbolAtLocation(node.arguments.nodes[0]); const ownModule = modSym !== undefined && lowerer.checker @@ -684,7 +684,7 @@ function collectDynamicImports(lowerer: Lowerer, sf: ts.SourceFile): void { * and kept .json files out of the module order. */ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) { - for (const stmt of fp.sf.statements) { + for (const stmt of fp.sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; if (!clause?.name || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; @@ -1071,8 +1071,8 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi ts.isPropertyAccessExpression(e.expression) && ["freeze", "seal", "preventExtensions"].includes(e.expression.name.text) && lowerer.stdlibGlobalMember(e.expression, "Object") !== null && - e.arguments[0] && - ts.isArrayLiteralExpression(e.arguments[0]) + e.arguments.nodes[0] && + ts.isArrayLiteralExpression(e.arguments.nodes[0]) ) return true; // The built-in Array constructor can create an evolving array whose @@ -1165,7 +1165,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi } if ( (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) && - !e.typeParameters?.length && + !e.typeParameters?.nodes.length && isJsSourceFile(e.getSourceFile()) ) return true; @@ -1287,7 +1287,7 @@ export function collectGlobals( !isJsSourceFile(sf) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || - inner.members.some((m) => decoratorNodesOf(m).length > 0)) && + inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && guaranteedDecorationThrow(lowerer, inner) !== null ) { continue; @@ -1470,7 +1470,7 @@ export function collectGlobals( !( requireSpecOf(rhs) !== null && inlineProgramRequireOf(lowerer, rhs) !== null && - !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member") ) && cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { @@ -1501,7 +1501,7 @@ export function collectGlobals( continue; } if (cjs?.kind === "table" && cjs.obj) { - for (const prop of cjs.obj.properties) { + for (const prop of cjs.obj.properties.nodes) { if (ts.isShorthandPropertyAssignment(prop)) continue; // alias plumbing if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) continue; if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { @@ -1579,8 +1579,8 @@ export function collectGlobals( if ( isJsSourceFile(sf) && (isDeadTscHelperDecl(lowerer, stmt) || - (stmt.declarationList.declarations.length === 1 && - tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + (stmt.declarationList.declarations.nodes.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations.nodes[0]!) !== null)) ) { continue; } @@ -1595,14 +1595,14 @@ export function collectGlobals( isRequireStatement(stmt) && !( ts.isVariableStatement(stmt) && - stmt.declarationList.declarations.some( + stmt.declarationList.declarations.nodes.some( (decl) => isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl), ) ) ) { if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } @@ -1614,7 +1614,7 @@ export function collectGlobals( const isLetList = (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { for (const element of requireNestedPatternsOf(decl)) { for (const nameNode of boundIdentifiersOf(element.name!)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); @@ -1646,7 +1646,8 @@ export function collectGlobals( // Node's ReferenceError at the access (lower-exprs), and the // statement itself lowers to nothing. if ( - ts.getCombinedModifierFlags(stmt.declarationList.declarations[0]!) & ts.ModifierFlags.Ambient + ts.getCombinedModifierFlags(stmt.declarationList.declarations.nodes[0]!) & + ts.ModifierFlags.Ambient ) continue; const list = stmt.declarationList; @@ -1661,8 +1662,8 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations[0]); - for (const decl of list.declarations) { + const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement // lowering emits nothing by the same test — see @@ -2010,7 +2011,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.some( + decl.initializer.properties.nodes.some( (p) => !ts.isSpreadAssignment(p) && (ts.isMethodDeclaration(p) || @@ -2057,7 +2058,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.every((p) => { + decl.initializer.properties.nodes.every((p) => { if (ts.isSpreadAssignment(p)) { return ( ts.isIdentifier(p.expression) && @@ -2181,7 +2182,7 @@ export function collectGlobals( const generic = member && lowerer.genericFnOf(member); const fallback = (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) && - !initializer.typeParameters?.length + !initializer.typeParameters?.nodes.length ? lowerer.lambdaSignature(initializer).funcType : member && lowerer.globalOf(member)?.type.kind === "dyn" ? DYN @@ -2274,7 +2275,7 @@ export function collectGlobals( !hasJsTypeAnnotation(decl) && decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) && - !decl.initializer.typeParameters?.length + !decl.initializer.typeParameters?.nodes.length ) { type = lowerer.lambdaSignature(decl.initializer).funcType; } @@ -2348,7 +2349,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) && ts.isPropertyAccessExpression(decl.initializer.expression) && lowerer.stdlibGlobalMember(decl.initializer.expression, "Object") === "assign" && - decl.initializer.arguments.some( + decl.initializer.arguments.nodes.some( (arg) => ts.isIdentifier(arg) && lowerer.globalOf(arg)?.type.kind === "dyn", ) ) { @@ -2601,7 +2602,7 @@ function collectNestedVarGlobals( return undefined; }); for (const list of lists) { - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { @@ -2707,7 +2708,7 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): !isJsSourceFile(stmt.getSourceFile()) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || - inner.members.some((m) => decoratorNodesOf(m).length > 0)) && + inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && guaranteedDecorationThrow(lowerer, inner) !== null ) { return { kind: "exprStmt", expr: lowerer.lowerExpr(stmt.expression), loc: locOf(stmt) }; diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 69d050510..7c1d70ed0 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -77,7 +77,7 @@ function nsTypeOnlyBody(decl: ts.ModuleDeclaration): boolean { while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined) return true; if (!ts.isModuleBlock(body)) return true; - return body.statements.every( + return body.statements.nodes.every( (s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || @@ -129,7 +129,7 @@ export function collectNamespaceStmt( while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return; // bodyless non-ambient: tsc-rejected lowerer.nsBlocks.set(body, "flattened"); - for (const s of body.statements) { + for (const s of body.statements.nodes) { if (ts.isFunctionDeclaration(s)) fp.fnDecls.push(s); else if (ts.isClassDeclaration(s)) fp.classDecls.push(s); else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) continue; @@ -146,7 +146,7 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return; lowerer.nsBlocks.set(body, "typeOnly"); - for (const s of body.statements) { + for (const s of body.statements.nodes) { if (ts.isModuleDeclaration(s)) registerTypeOnlyBlocks(lowerer, s); } } diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 292b38e87..68f11b948 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -557,10 +557,10 @@ export function lowerRequireCacheKeys( call: ts.CallExpression, member: string, ): IrExpr | null { - const argument = call.arguments[0]; + const argument = call.arguments.nodes[0]; if ( member !== "keys" || - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || argument === undefined || !isRequireCacheExpr(lowerer, argument) ) diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index d46e7244f..aedd123c6 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -708,7 +708,7 @@ export function lowerNetModuleCall( // Members with ordinary surfaces.ts table entries (the autoselect // budget pair) ride the generic path — null hands the dispatch back. if (builtinModuleFnOf(lowerer, "net", bi.member)) return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length === 0) { return { kind: "libCall", fn: "net.createServer", args: [], type: NETSERVER_T, loc }; @@ -883,7 +883,7 @@ function lowerNetConnectOptions( member: string, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const optsNode = args[0] as ts.ObjectLiteralExpression; const isJs = isJsSourceFile(expr.getSourceFile()); // The checked-dynamic option-bag route (JS sources): a literal with @@ -893,7 +893,7 @@ function lowerNetConnectOptions( // port/host/autoSelectFamily), with the compiler-rendered fence as the // post-validation tail. if (isJs) { - const needsBag = optsNode.properties.some( + const needsBag = optsNode.properties.nodes.some( (p) => (!ts.isPropertyAssignment(p) && !ts.isShorthandPropertyAssignment(p)) || (ts.isPropertyAssignment(p) && ts.isComputedPropertyName(p.name)) || @@ -944,7 +944,7 @@ function lowerNetConnectOptions( type: NETSOCKET_T, loc, }); - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -1147,9 +1147,9 @@ function lowerServerCloseBind( if (!lowerer.isStdlibMember(closeAccess)) return null; const loc = locOf(call); const recvNode = closeAccess.expression; - const thisArg = call.arguments[0]; + const thisArg = call.arguments.nodes[0]; if ( - call.arguments.length !== 1 || + call.arguments.nodes.length !== 1 || !thisArg || !ts.isIdentifier(recvNode) || !ts.isIdentifier(thisArg) || @@ -1352,7 +1352,7 @@ function lowerNetServerMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "getConnections") { // cb(null, count) on the next tick; answers the server. if (args.length !== 1) { @@ -1451,7 +1451,7 @@ function lowerNetServerMethodCall( let host: IrExpr | null = null; let v6only: IrExpr | null = null; let reusePort: IrExpr | null = null; - for (const prop of (args[0] as ts.ObjectLiteralExpression).properties) { + for (const prop of (args[0] as ts.ObjectLiteralExpression).properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -2152,8 +2152,8 @@ function lowerEncodedDataListener(lowerer: Lowerer, node: ts.Expression): IrExpr let fn = node; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return null; - if (fn.parameters.length !== 1) return null; - const param = fn.parameters[0]!; + if (fn.parameters.nodes.length !== 1) return null; + const param = fn.parameters.nodes[0]!; if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return null; const symbol = lowerer.checker.getSymbolAtLocation(param.name); if (symbol === undefined) return null; @@ -2184,7 +2184,7 @@ function lowerNetSocketMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if ((name === "ref" || name === "unref") && args.length === 0) { // Whether the socket keeps the event loop alive. requireStatementPosition(lowerer, call, `socket.${name}()`); @@ -2868,7 +2868,7 @@ function h2EndStreamOption(lowerer: Lowerer, node: ts.Expression | undefined): b ); } let end: boolean | undefined; - for (const prop of (node as ts.ObjectLiteralExpression).properties) { + for (const prop of (node as ts.ObjectLiteralExpression).properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { lowerer.noLowering( "h2 options with spreads or computed keys", @@ -2907,7 +2907,7 @@ function lowerH2SessionMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = () => coerceToHandle(lowerer, access.expression, HTTP2SESSION_T); if (name === "request") { const pairs = @@ -3169,7 +3169,7 @@ function lowerH2StreamMethodCall( if (!lowerer.isStdlibMember(access) && !compatReqStream) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const receiver = () => receiverOverride ?? coerceToHandle(lowerer, access.expression, HTTP2STREAM_T); if (name === "respond") { @@ -3949,7 +3949,7 @@ export function lowerServerProperty( } if (expr.name.text !== "port") return null; const recv = expr.expression; - if (!ts.isCallExpression(recv) || recv.questionDotToken || recv.arguments.length !== 0) + if (!ts.isCallExpression(recv) || recv.questionDotToken || recv.arguments.nodes.length !== 0) return null; if (!ts.isPropertyAccessExpression(recv.expression)) return null; const inner = recv.expression; @@ -4051,7 +4051,7 @@ function lowerHttpServerOptions( let allowMissingHost = false; let keepAliveTimeoutBuffer: IrExpr | null = null; let maxHeaderSize: IrExpr | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -4169,7 +4169,7 @@ function lowerHttpCreateServerForms( what: string, loc: SrcLoc, ): IrExpr { - const args: readonly ts.Expression[] = expr.arguments ?? []; + const args: readonly ts.Expression[] = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -4335,7 +4335,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE let timeout: IrExpr = numLit(-1, loc); let port: IrExpr = numLit(-1, loc); let scheduling: IrExpr = lowerer.coerceToExpected(strLit("lifo", loc), DYN); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `${api} with ${args.length} arguments`, @@ -4352,7 +4352,7 @@ export function lowerHttpAgentNew(lowerer: Lowerer, expr: ts.NewExpression): IrE "spell the options as an object literal at the construction site", ); } - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { if (ts.isSpreadAssignment(prop)) { lowerer.noLowering(`${api} with an options spread`, prop, "write each option inline"); } @@ -4485,9 +4485,9 @@ function lowerHttpModuleCall( if (bi.member === "validateHeaderName") { requireStatementPosition(lowerer, expr, "http.validateHeaderName(...)"); if ( - expr.arguments.length < 1 || - expr.arguments.length > 2 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 2 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "http.validateHeaderName arguments", @@ -4495,24 +4495,24 @@ function lowerHttpModuleCall( "pass a header name and an optional label", ); } - const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const name = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const label = - expr.arguments.length === 2 - ? lowerer.lowerExprExpecting(expr.arguments[1]!, STRING) + expr.arguments.nodes.length === 2 + ? lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING) : strLit("Header name", loc); return { kind: "libCall", fn: "http.validateHeaderName", args: [name, label], type: VOID, loc }; } if (bi.member === "validateHeaderValue") { requireStatementPosition(lowerer, expr, "http.validateHeaderValue(...)"); - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "http.validateHeaderValue arguments", expr, "pass a header name and a value", ); } - const name = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const value = lowerer.lowerExprExpecting(expr.arguments[1]!, DYN); + const name = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const value = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, DYN); return { kind: "libCall", fn: "http.validateHeaderValue", @@ -4556,7 +4556,7 @@ function lowerTlsServerOptions( const loc = locOf(node); let cert: IrExpr | null = null; let key: IrExpr | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { let initializer: ts.Expression | null; if ( ts.isPropertyAssignment(prop) && @@ -4681,7 +4681,7 @@ function tlsLiteralNeedsRuntimeWalk( includeHttpTimeoutOptions = false, ): boolean { const jsSource = isJsSourceFile(node.getSourceFile()); - return node.properties.some( + return node.properties.nodes.some( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && @@ -4735,7 +4735,7 @@ function lowerTlsServerOptionsOrDyn( * callback (and 'secureConnect'/'connect' listeners) fires * post-handshake — Node's secureConnect timing. */ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; const FORMS_HINT = "the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername"; if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) { @@ -4847,7 +4847,7 @@ function lowerTlsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -5178,7 +5178,7 @@ function lowerHttpsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -5279,7 +5279,7 @@ function lowerHttp2ModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { // The REAL h2c server (scr_http2.c). An options object literal is // accepted and ignored (h2 session-tuning knobs; the honest-defaults @@ -5355,7 +5355,7 @@ function lowerHttp2ModuleCall( lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!)).length === 0 ) { if (ts.isObjectLiteralExpression(args[1]!)) { - for (const pr of (args[1] as ts.ObjectLiteralExpression).properties) { + for (const pr of (args[1] as ts.ObjectLiteralExpression).properties.nodes) { // Plain `name: value` and shorthand `{ ca }` both carry the // option; a shorthand's value is the same-named identifier. let name: string; @@ -5570,14 +5570,14 @@ function lowerHttp2ModuleCall( const ok = ts.isObjectLiteralExpression(whenTrue) && ts.isObjectLiteralExpression(whenFalse) && - whenFalse.properties.length === 0 && - whenTrue.properties.length === 1 && - ts.isPropertyAssignment(whenTrue.properties[0]!) && - (ts.isIdentifier(whenTrue.properties[0]!.name) || - ts.isStringLiteral(whenTrue.properties[0]!.name)) && - (whenTrue.properties[0]!.name as ts.Identifier | ts.StringLiteral).text === + whenFalse.properties.nodes.length === 0 && + whenTrue.properties.nodes.length === 1 && + ts.isPropertyAssignment(whenTrue.properties.nodes[0]!) && + (ts.isIdentifier(whenTrue.properties.nodes[0]!.name) || + ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) && + (whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text === "SNICallback" && - whenTrue.properties[0]!.initializer.getText() === condText; + whenTrue.properties.nodes[0]!.initializer.getText() === condText; if (!ok) { lowerer.noLowering( "createSecureServer options with a conditional spread", @@ -5595,7 +5595,7 @@ function lowerHttp2ModuleCall( `spreads must be inline object literals here; ${SNI_HINT}`, ); } - for (const p of inner.properties) lowerOption(p); + for (const p of inner.properties.nodes) lowerOption(p); return; } let initializer: ts.Expression | null; @@ -5671,14 +5671,14 @@ function lowerHttp2ModuleCall( e.kind === ts.SyntaxKind.FalseKeyword; const isLiteralObjectOfLiterals = (e: ts.Expression): boolean => ts.isObjectLiteralExpression(e) && - e.properties.every( + e.properties.nodes.every( (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && isScalarLiteral(p.initializer), ); if (k === "settings" && initializer !== null && ts.isObjectLiteralExpression(initializer)) { - for (const setting of initializer.properties) { + for (const setting of initializer.properties.nodes) { if ( ts.isPropertyAssignment(setting) && ts.isIdentifier(setting.name) && @@ -5731,7 +5731,7 @@ function lowerHttp2ModuleCall( ); // An undocumented key, dropped like Node drops it. }; - for (const prop of optsNode.properties) lowerOption(prop); + for (const prop of optsNode.properties.nodes) lowerOption(prop); if (cert === null || key === null) { lowerer.noLowering( "createSecureServer without both cert and key", @@ -5846,7 +5846,7 @@ function lowerHttpClientCall( const c = lowerer.lowerCondition(binding!.cond); return binding!.trueSecure ? c : { kind: "unary", op: "!", operand: c, type: BOOL, loc }; }; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length < 1 || args.length > 2) { lowerer.noLowering( `${member} with ${args.length} arguments`, @@ -5934,7 +5934,7 @@ function lowerHttpClientCall( let servername: IrExpr | null = null; /** maxHeaderSize: the response head's size bound. */ let maxHeaderSize: IrExpr | null = null; - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { // Shorthand entries ({ port } for { port: port }) are the natural // spelling at these call sites — the shorthand's VALUE lowering // resolves the local binding (lowerShorthandValue), not the property @@ -6441,7 +6441,7 @@ function lowerClientHeadersOption(lowerer: Lowerer, node: ts.Expression): IrExpr const loc = locOf(node); if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -6493,8 +6493,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr if (!ts.isArrayLiteralExpression(node)) return lowerClientHeadersOption(lowerer, node); const loc = locOf(node); const flat: IrExpr[] = []; - for (const entry of node.elements) { - if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2) { + for (const entry of node.elements.nodes) { + if (!ts.isArrayLiteralExpression(entry) || entry.elements.nodes.length !== 2) { lowerer.noLowering( "addTrailers pair list", entry, @@ -6502,8 +6502,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr ); } const pair = entry as ts.ArrayLiteralExpression; - const name = pair.elements[0]!; - const value = pair.elements[1]!; + const name = pair.elements.nodes[0]!; + const value = pair.elements.nodes[1]!; if (ts.isSpreadElement(name) || ts.isSpreadElement(value)) { lowerer.noLowering( "addTrailers pair with spread", @@ -6528,7 +6528,7 @@ function lowerHttpClientMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setHeader") { requireStatementPosition(lowerer, call, "request.setHeader(...)"); if (args.length !== 2) @@ -7038,7 +7038,7 @@ function lowerHttpReqMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { requireStatementPosition(lowerer, call, "message.setTimeout(...)"); if (args.length < 1 || args.length > 2) @@ -7239,7 +7239,7 @@ function lowerHttpResMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -7298,7 +7298,7 @@ function lowerHttpResMethodCall( let pairs: IrExpr; if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -7542,7 +7542,7 @@ function lowerHttpResMethodCall( } const names: IrExpr[] = []; const values: IrExpr[] = []; - for (const prop of headersNode.properties) { + for (const prop of headersNode.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 9fbe87c3d..92d9f0690 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -214,8 +214,8 @@ function visitRequestInitConstSelectedValues( // a statically traced spread) hides earlier values exactly as it does at // runtime; a conditional/missing spread keeps the earlier contributor // in the possible-value set. - for (let i = expr.properties.length - 1; i >= 0; i--) { - const property = expr.properties[i]!; + for (let i = expr.properties.nodes.length - 1; i >= 0; i--) { + const property = expr.properties.nodes[i]!; if (ts.isSpreadAssignment(property)) { const defines = visitRequestInitConstSelectedValues( lowerer, @@ -238,7 +238,7 @@ function visitRequestInitConstSelectedValues( return false; } if (selector.kind === "index" && ts.isArrayLiteralExpression(expr)) { - const element = expr.elements[selector.index]; + const element = expr.elements.nodes[selector.index]; if (element === undefined || ts.isOmittedExpression(element) || ts.isSpreadElement(element)) { return false; } @@ -343,7 +343,7 @@ function requestInitConstBindingSource( selectors.unshift({ kind: "property", key }); } else if (ts.isArrayBindingPattern(pattern)) { if (current.dotDotDotToken) return null; - const index = pattern.elements.indexOf(current); + const index = pattern.elements.nodes.indexOf(current); if (index < 0) return null; selectors.unshift({ kind: "index", index }); } else { @@ -464,8 +464,8 @@ function fenceRequestInitObject( // effective contributor for each dictionary member; a definite later // `cache: undefined`, for example, suppresses an earlier unsupported // cache value just as WebIDL observes at runtime. - for (let index = init.properties.length - 1; index >= 0; index--) { - const prop = init.properties[index]!; + for (let index = init.properties.nodes.length - 1; index >= 0; index--) { + const prop = init.properties.nodes[index]!; if (ts.isSpreadAssignment(prop)) { for (const row of rows) { if (shadowed.has(row.member)) continue; @@ -569,12 +569,12 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp if (call.questionDotToken) return null; const symbol = lowerer.resolveValueSymbol(callee); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - if (call.arguments.length < 1) return null; + if (call.arguments.nodes.length < 1) return null; const loc = locOf(call); - const initNode = call.arguments[1]; + const initNode = call.arguments.nodes[1]; if (initNode !== undefined) fenceRequestInitValue(lowerer, initNode); { - const inputNode = call.arguments[0]!; + const inputNode = call.arguments.nodes[0]!; const input = lowerer.lowerExpr(inputNode); let init: IrExpr; if (initNode === undefined) { @@ -608,7 +608,7 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp { kind: "varDecl", localId: inputLocal.id, init: input, loc }, { kind: "varDecl", localId: initLocal.id, init, loc }, ]; - for (const argument of call.arguments.slice(2)) { + for (const argument of call.arguments.nodes.slice(2)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on static fetch()", @@ -660,7 +660,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio if ( access.questionDotToken || (member !== "json" && member !== "text" && member !== "bytes" && member !== "arrayBuffer") || - call.arguments.length !== 0 + call.arguments.nodes.length !== 0 ) { return null; } @@ -1132,7 +1132,7 @@ export function fenceFetchObjectBinding(lowerer: Lowerer, pattern: ts.ObjectBind fenceFetchObjectMembers( lowerer, pattern, - pattern.elements.flatMap((element) => { + pattern.elements.nodes.flatMap((element) => { if (element.dotDotDotToken) return []; const member = fetchObjectBindingMemberName(lowerer, element); return member === null ? [] : [{ member, node: element }]; @@ -1151,7 +1151,7 @@ export function fenceFetchObjectAssignment( fenceFetchObjectMembers( lowerer, source, - target.properties.flatMap((property) => { + target.properties.nodes.flatMap((property) => { const member = fetchObjectAssignmentMemberName(lowerer, property); return member === null ? [] : [{ member, node: property }]; }), @@ -1195,7 +1195,7 @@ export function lowerFetchElementMethodCall( call.questionDotToken || !ts.isElementAccessExpression(call.expression) || call.expression.questionDotToken || - call.arguments.some((argument) => ts.isSpreadElement(argument)) + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ) { return null; } @@ -1233,7 +1233,7 @@ export function lowerFetchElementMethodCall( const loc = locOf(call); const receiverLocal = lowerer.declareHiddenLocal("%fetchReceiver", DYN); const keyLocal = lowerer.declareHiddenLocal("%fetchMember", STRING); - const argumentValues = call.arguments.map((argument, index) => + const argumentValues = call.arguments.nodes.map((argument, index) => fetchMethodArgumentIsLive(resolved.owner, members, index) ? lowerLiveWebValue(lowerer, argument) : lowerer.lowerExprExpecting(argument, DYN), @@ -1272,7 +1272,7 @@ export function lowerFetchElementMethodCall( })); const staticResponsePromise: IrType | null = resolved.owner === "Response" && - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && members.every((member) => member === "text" || member === "bytes") ? (() => { // The checker spells this call as @@ -1409,7 +1409,7 @@ function staticFirstArgWithSurplusArgs( first: IrExpr, ): IrExpr { const loc = locOf(call); - if (call.arguments.length <= 1) return first; + if (call.arguments.nodes.length <= 1) return first; const firstLocal = lowerer.declareHiddenLocal("%fetchArg", first.type); const firstRef: IrExpr = { kind: "varRef", @@ -1418,7 +1418,7 @@ function staticFirstArgWithSurplusArgs( loc, }; const stmts: IrStmt[] = [{ kind: "varDecl", localId: firstLocal.id, init: first, loc }]; - for (const argument of call.arguments.slice(1)) { + for (const argument of call.arguments.nodes.slice(1)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on the static fetch companion APIs", @@ -1472,14 +1472,14 @@ export function lowerStaticFetchCompanionCall( noteEventListeners(lowerer, loc); switch (call.expression.name.text) { case "timeout": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1490,7 +1490,7 @@ export function lowerStaticFetchCompanionCall( }), ); case "abort": - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "libCall", fn: "fetch.abortNow", @@ -1499,11 +1499,11 @@ export function lowerStaticFetchCompanionCall( loc, }; } - if (ts.isSpreadElement(call.arguments[0]!)) return null; + if (ts.isSpreadElement(call.arguments.nodes[0]!)) return null; return staticCallWithSurplusArgs( lowerer, call, - lowerLiveWebValue(lowerer, call.arguments[0]!), + lowerLiveWebValue(lowerer, call.arguments.nodes[0]!), (first) => ({ kind: "libCall", fn: "fetch.abortNow", @@ -1513,14 +1513,14 @@ export function lowerStaticFetchCompanionCall( }), ); case "any": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1537,9 +1537,9 @@ export function lowerStaticFetchCompanionCall( if ( root.text === "ReadableStream" && call.expression.name.text === "from" && - (!call.arguments[0] || !ts.isSpreadElement(call.arguments[0])) + (!call.arguments.nodes[0] || !ts.isSpreadElement(call.arguments.nodes[0])) ) { - if (!call.arguments[0]) { + if (!call.arguments.nodes[0]) { return { kind: "libCall", fn: "fetch.streamFrom", @@ -1548,7 +1548,7 @@ export function lowerStaticFetchCompanionCall( loc, }; } - let source = lowerer.lowerExpr(call.arguments[0]!); + let source = lowerer.lowerExpr(call.arguments.nodes[0]!); // A readonly tuple satisfies the fallback's readonly-array overload, // but maps to a monomorphic record in IR. Reuse the ordinary // tuple→array width conversion with the resolved generic parameter so @@ -1576,8 +1576,8 @@ export function lowerStaticFetchCompanionCall( source.type.kind === "dyn"; if (!preserve) { lowerer.noLowering( - `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))} iterables`, - call.arguments[0]!, + `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments.nodes[0]!))} iterables`, + call.arguments.nodes[0]!, "arrays, Uint8Array, and strings are supported — spread other synchronous iterables into an array first: [...iterable]", ); } @@ -1607,7 +1607,7 @@ export function lowerStaticAbortControllerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.some((argument) => ts.isSpreadElement(argument)) || + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken || staticResponseMemberName(lowerer, access) !== "abort" @@ -1627,8 +1627,8 @@ export function lowerStaticAbortControllerCall( const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); - const first = call.arguments[0] - ? lowerLiveWebValue(lowerer, call.arguments[0]) + const first = call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) : dynUndefinedExpr(loc); const args = [staticFirstArgWithSurplusArgs(lowerer, call, first)]; return lowerStaticFixedFetchMethodCall( @@ -1660,7 +1660,7 @@ export function lowerStaticReadableStreamControllerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length > 1 || + call.arguments.nodes.length > 1 || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -1682,7 +1682,9 @@ export function lowerStaticReadableStreamControllerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1713,8 +1715,8 @@ export function lowerStaticReadableStreamCancelCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length > 1 || - call.arguments.some((argument) => ts.isSpreadElement(argument)) || + call.arguments.nodes.length > 1 || + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken || staticResponseMemberName(lowerer, access) !== "cancel" @@ -1729,7 +1731,9 @@ export function lowerStaticReadableStreamCancelCall( if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1761,8 +1765,8 @@ export function lowerStaticAbortSignalListenerCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length < 2 || - call.arguments.some((arg) => ts.isSpreadElement(arg)) || + call.arguments.nodes.length < 2 || + call.arguments.nodes.some((arg) => ts.isSpreadElement(arg)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -1786,7 +1790,7 @@ export function lowerStaticAbortSignalListenerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); noteEventListeners(lowerer, loc); - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => index === 1 ? lowerLiveWebValue(lowerer, arg) : lowerer.lowerExprExpecting(arg, DYN), ); return lowerStaticFixedFetchMethodCall( @@ -1814,7 +1818,7 @@ export function lowerStaticAbortSignalListenerCall( export function lowerEventTargetNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "EventTarget" && name !== "Event" && name !== "CustomEvent") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); if (name === "EventTarget") noteEventListeners(lowerer, loc); @@ -1849,7 +1853,7 @@ export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some((argument) => ts.isSpreadElement(argument))) return null; const loc = locOf(expr); noteEventListeners(lowerer, loc); @@ -1884,7 +1888,7 @@ export function lowerStaticReadableStreamNew( } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) return null; const loc = locOf(expr); @@ -1907,7 +1911,7 @@ export function lowerStaticReadableStreamNew( export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "Request" && name !== "Headers") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); @@ -1931,7 +1935,7 @@ export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; return lowerResponseConstruction(lowerer, args, locOf(expr), false); } @@ -1944,8 +1948,9 @@ export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpressio const access = call.expression; if (access.questionDotToken || access.name.text !== "json") return null; if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null; - if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null; - return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true); + if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length === 0) + return null; + return lowerResponseConstruction(lowerer, call.arguments.nodes, locOf(call), true); } function lowerResponseConstruction( @@ -2110,7 +2115,7 @@ export function lowerStaticReadableStreamReaderCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length !== 0 || + call.arguments.nodes.length !== 0 || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken ) { @@ -2152,7 +2157,7 @@ export function lowerStaticReadableStreamReaderCall( export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) { lowerer.unsupported( "SC1090", @@ -2161,7 +2166,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression "build time — the specifier must be a string literal)", ); } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.unsupported("SC1090", call, "dynamic import() with import attributes"); } const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`); @@ -2354,14 +2359,14 @@ export function lowerStdlibMethodCall( const sigMember = lowerer.stdlibGlobalMember(access, "AbortSignal"); if ( (sigMember === "timeout" || sigMember === "abort" || sigMember === "any") && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi(`'AbortSignal.${sigMember}'`, call); } } if ( lowerer.stdlibGlobalMember(access, "ReadableStream") === "from" && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi("'ReadableStream.from'", call); } @@ -2374,10 +2379,10 @@ export function lowerStdlibMethodCall( // for undefined, instead of trusting the checker's number type. const mathArg = (a: ts.Expression): IrExpr => lowerToNumberArgument(lowerer, a); if (staticMath && name === "hypot") { - const elems = call.arguments.map((a) => + const elems = call.arguments.nodes.map((a) => ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : mathArg(a), ); - const spreads = call.arguments.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); + const spreads = call.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); const packed: IrExpr = { kind: "arrayLit", elems, @@ -2387,7 +2392,7 @@ export function lowerStdlibMethodCall( }; return { kind: "libCall", fn: staticMath.fn, args: [packed], type: F64, loc }; } - if (staticMath && call.arguments.every((a) => !ts.isSpreadElement(a))) { + if (staticMath && call.arguments.nodes.every((a) => !ts.isSpreadElement(a))) { // Math.max/Math.min at ANY plain arity — Node's are variadic. The // spec's reduction is a left fold of the same NaN-poisoning // ±0-ordered scalar compare the two-arg form lowers to, so n @@ -2398,18 +2403,18 @@ export function lowerStdlibMethodCall( // arguments are the fold's seed: -Infinity for max, +Infinity for // min, exactly Node. if (name === "max" || name === "min") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "numLit", value: name === "max" ? -Infinity : Infinity, type: F64, loc }; } - const args = call.arguments.map(mathArg); + const args = call.arguments.nodes.map(mathArg); let acc = args[0]!; for (let i = 1; i < args.length; i++) { acc = { kind: "libCall", fn: staticMath.fn, args: [acc, args[i]!], type: F64, loc }; } return acc; } - if (call.arguments.length === staticMath.arity) { - const args = call.arguments.map(mathArg); + if (call.arguments.nodes.length === staticMath.arity) { + const args = call.arguments.nodes.map(mathArg); return { kind: "libCall", fn: staticMath.fn, args, type: F64, loc }; } } @@ -2420,10 +2425,10 @@ export function lowerStdlibMethodCall( if ( isMath && (name === "max" || name === "min") && - call.arguments.length === 1 && - ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + ts.isSpreadElement(call.arguments.nodes[0]!) ) { - const spread = call.arguments[0]! as ts.SpreadElement; + const spread = call.arguments.nodes[0]! as ts.SpreadElement; let src = lowerer.lowerExpr(spread.expression); // JavaScript array call results retain checked storage so aliases stay // shared. This read-only fold can extract the inferred numeric array. @@ -2449,7 +2454,7 @@ export function lowerStdlibMethodCall( }; } const mathFn = isMath ? own(STDLIB_SURFACE.math.fns, name) : undefined; - if (mathFn && call.arguments.length === mathFn.args.length) { + if (mathFn && call.arguments.nodes.length === mathFn.args.length) { lowerer.refuseUnsupportedApi(`'Math.${name}'`, call); } const kind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; @@ -2459,7 +2464,7 @@ export function lowerStdlibMethodCall( : kind === "string" ? own(STDLIB_SURFACE.string, name) : undefined; - if (!entry || call.arguments.length !== entry.args.length) return null; + if (!entry || call.arguments.nodes.length !== entry.args.length) return null; if (!lowerer.isStdlibMember(access)) return null; lowerer.refuseUnsupportedApi(`'.${name}()' on ${kind === "f64" ? "numbers" : "strings"}`, call); } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 3a0958ae6..53e41f259 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -242,7 +242,7 @@ function npmStaticReturnedEmptyArrayType( !ts.isIdentifier(decl.name) || decl.initializer === undefined || !ts.isArrayLiteralExpression(decl.initializer) || - decl.initializer.elements.length !== 0 || + decl.initializer.elements.nodes.length !== 0 || !isJsSourceFile(decl.getSourceFile()) || npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || lowerer.ctx.returnType.kind !== "array" @@ -568,7 +568,7 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin ); } const registrations: UsingRegistration[] = []; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); if (decl.initializer === undefined) lowerer.unsupported("SC1090", decl, "a 'using' declaration without an initializer"); @@ -596,7 +596,7 @@ function lowerUsingStatement(lowerer: Lowerer, stmt: ts.VariableStatement): Usin function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean { if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return false; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length === 0) return false; return decls.every((d) => { let init = d.initializer; @@ -842,7 +842,7 @@ function isAbruptStatement(stmt: ts.Statement): boolean { function skipsWhenUnreachable(stmt: ts.Statement): boolean { if (ts.isFunctionDeclaration(stmt)) return stmt.typeParameters === undefined; if (ts.isVariableStatement(stmt)) - return !(stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword); + return !(stmt.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword); return !( ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt) || @@ -991,7 +991,8 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b * resume the caller and thread its captures normally. */ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): boolean { const decl = lowerer.checker.valueDeclarationOf(symbol); - if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters) return false; + if (!decl || !ts.isFunctionDeclaration(decl) || !decl.body || decl.typeParameters?.nodes) + return false; for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) { const entry = lowerer.activeStmtLists[i]!; const idx = entry.stmts.indexOf(decl); @@ -1258,7 +1259,7 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void list = stmt.initializer; } if (!list) return; - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol) lowerer.blockedBindings.add(symbol); @@ -1286,7 +1287,7 @@ export function lowerScopedBlock(lowerer: Lowerer, stmt: ts.Statement): IrStmt[] lowerer.scopes.push(new Map()); try { return withRuntimeOptionalScope(lowerer, () => - ts.isBlock(stmt) ? lowerer.lowerStmts(stmt.statements) : lowerer.lowerStmts([stmt]), + ts.isBlock(stmt) ? lowerer.lowerStmts(stmt.statements.nodes) : lowerer.lowerStmts([stmt]), ); } finally { lowerer.scopes.pop(); @@ -1359,7 +1360,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt if (ts.isVariableStatement(stmt)) { // `declare const` — ambient: no storage, no init (collectGlobals // skipped it; reads throw Node's ReferenceError at the access). - const first = stmt.declarationList.declarations[0]; + const first = stmt.declarationList.declarations.nodes[0]; if (first && ts.getCombinedModifierFlags(first) & ts.ModifierFlags.Ambient) return null; return lowerer.lowerVarStatement(stmt); } @@ -1603,7 +1604,7 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement): const isConst = (list.flags & ts.NodeFlags.Const) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - return list.declarations.flatMap((decl) => { + return list.declarations.nodes.flatMap((decl) => { const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet); // A require binding earlier code can read is initialized from here on // (require-initialization.ts). @@ -1765,7 +1766,7 @@ export function numericIteratorSourceOf( if (expr === undefined) return null; let init = expr; while (ts.isParenthesizedExpression(init)) init = init.expression; - if (!ts.isCallExpression(init) || init.questionDotToken || init.arguments.length !== 0) + if (!ts.isCallExpression(init) || init.questionDotToken || init.arguments.nodes.length !== 0) return null; let receiver: ts.Expression | null = null; @@ -1872,7 +1873,7 @@ function lowerMatchAllDrainDecl( const loc = locOf(decl); const access = call.expression as ts.PropertyAccessExpression; const receiver = lowerer.lowerExpr(access.expression); - const re = lowerer.lowerExpr(call.arguments[0]!); + const re = lowerer.lowerExpr(call.arguments.nodes[0]!); const idxsT = arrayOf(F64); const idxs = lowerer.declareHiddenLocal("%midxs", idxsT); const local = lowerer.declareLocal(decl.name, decl.name.text, rowsT, false); @@ -2132,11 +2133,11 @@ export function matchGroupsPatternSource( ): IrExpr | null { if ( !ts.isObjectBindingPattern(pattern) || - pattern.elements.length === 0 || + pattern.elements.nodes.length === 0 || !(row.type.kind === "dyn" ? isJsSourceFile(pattern.getSourceFile()) : isMatchSliceType(lowerer, row.type)) || - !pattern.elements.every((el) => { + !pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined || el.name === undefined) return false; const prop = el.propertyName; if (prop === undefined) return ts.isIdentifier(el.name) && el.name.text === "groups"; @@ -2209,8 +2210,8 @@ export function lowerBindingPattern( srcType.kind === "union" && lowerer.unions.get(srcType.unionId)?.arms.every((arm) => arm.kind === "array") ) { - for (let i = 0; i < pattern.elements.length; i++) { - const el = pattern.elements[i]!; + for (let i = 0; i < pattern.elements.nodes.length; i++) { + const el = pattern.elements.nodes[i]!; if (ts.isOmittedExpression(el) || el.name === undefined) continue; if (el.dotDotDotToken) lowerer.unsupported("SC1031", el, "rest elements over a union of array layouts"); @@ -2231,7 +2232,7 @@ export function lowerBindingPattern( // shape — the positional twin of object destructuring below. const tupleShape = srcType.kind === "record" ? lowerer.shapes.get(srcType.shapeId) : undefined; if (srcType.kind === "record" && tupleShape?.tuple) { - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { // Holes: OmittedExpression in 5.9.3, a nameless BindingElement in // 7 (parity finding 2) — either way the position is skipped. if (ts.isOmittedExpression(el) || el.name === undefined) return; @@ -2306,7 +2307,7 @@ export function lowerBindingPattern( loc, }); const itRef = (): IrExpr => ({ kind: "varRef", localId: it.id, type: citPat.iterT, loc }); - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { const stepLoc = locOf(el); const step = lowerer.classIteratorNextCall(citPat, itRef(), stepLoc); if (ts.isOmittedExpression(el) || el.name === undefined) { @@ -2391,7 +2392,7 @@ export function lowerBindingPattern( out.push({ kind: "varDecl", localId: pack.id, - init: pattern.elements.some( + init: pattern.elements.nodes.some( (element) => ts.isBindingElement(element) && element.dotDotDotToken, ) ? { @@ -2404,14 +2405,14 @@ export function lowerBindingPattern( : { kind: "dynIterN", value: srcRef(), - count: pattern.elements.length, + count: pattern.elements.nodes.length, ...(dynSpell ? { notIterableMessage: dynSpell } : {}), type: DYN, loc, }, loc, }); - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; // hole: the position skips const elLoc = locOf(el); if (el.dotDotDotToken) { @@ -2475,7 +2476,7 @@ export function lowerBindingPattern( // fresh number[] (never another typed array), exactly the iterator // protocol's Array accumulation. if (srcType.kind === "bytes") { - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2523,7 +2524,7 @@ export function lowerBindingPattern( // like the string case above. Element defaults keep a fence: they // run between iterator steps and could mutate the live collection. if (srcType.kind === "map" || srcType.kind === "set") { - const defaulted = pattern.elements.find( + const defaulted = pattern.elements.nodes.find( (el): el is ts.BindingElement => ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken, ); @@ -2556,7 +2557,7 @@ export function lowerBindingPattern( `array destructuring of non-array values (the source is ${srcType.kind}-typed)`, ); } - pattern.elements.forEach((el, i) => { + pattern.elements.nodes.forEach((el, i) => { if (ts.isOmittedExpression(el) || el.name === undefined) return; // hole: position skipped, like JS over an array const loc = locOf(el); // Array rest drains values from the iterator, so holes become @@ -2593,7 +2594,7 @@ export function lowerBindingPattern( }); return; } - if (pattern.elements.length === 0) { + if (pattern.elements.nodes.length === 0) { // `var {} = e`, `for (const {} of ns)`: RequireObjectCoercible and // nothing else — a pure no-op past the source's own evaluation for // every statically non-nullish kind, record or not (the assignment @@ -2640,12 +2641,12 @@ export function lowerBindingPattern( return; } if (srcType.kind === "dyn") { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { const keys: IrExpr[] = []; - for (const sibling of pattern.elements) { + for (const sibling of pattern.elements.nodes) { if (sibling === el || sibling.dotDotDotToken) continue; const key = sibling.propertyName ?? sibling.name; const name = @@ -2733,7 +2734,7 @@ export function lowerBindingPattern( const info = lowerer.classes.get(srcType.className); if (!info) lowerer.flushDeferredClass(srcType.className); if (info) { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2742,7 +2743,7 @@ export function lowerBindingPattern( // CopyDataProperties copies own enumerable properties, which // for a class instance are exactly the fields). const consumed = new Set(); - for (const sib of pattern.elements) { + for (const sib of pattern.elements.nodes) { if (sib === el || sib.name === undefined || sib.dotDotDotToken) continue; const keyNode = sib.propertyName ?? (ts.isIdentifier(sib.name) ? sib.name : null); if (keyNode === null) continue; // defensive: a pattern target always carries a propertyName @@ -2850,7 +2851,7 @@ export function lowerBindingPattern( // naming that. Callers vetted the source as a checker-typed string — // builtin identity tokens fence before reaching here. if (srcType.kind === "string") { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -2917,7 +2918,7 @@ export function lowerBindingPattern( const shape = lowerer.shapes.get(srcType.shapeId); if (!shape) throw new InternalCompilerError(`lowerer bug: destructuring unknown shape ${srcType.shapeId}`); - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; // 7 spells elisions as nameless elements // `{ a, ...rest }`: the rest binds a FRESH record of the unconsumed // fields (JS's CopyDataProperties makes a fresh object too, own @@ -3209,7 +3210,7 @@ function lowerMatchResultObjectPattern( out: IrStmt[], ): void { let groups: { name: string; index: number }[] | null | undefined; - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const loc = locOf(el); if (el.dotDotDotToken) { @@ -3350,7 +3351,7 @@ function staticStringPattern( pattern: ts.ArrayBindingPattern | ts.ObjectBindingPattern, ): boolean { if (ts.isArrayBindingPattern(pattern)) { - return pattern.elements.every((el) => { + return pattern.elements.nodes.every((el) => { if (ts.isOmittedExpression(el) || el.name === undefined) return true; if (el.dotDotDotToken) { // The rest binds string[]: identifier targets and nested ARRAY @@ -3362,7 +3363,7 @@ function staticStringPattern( return staticStringPattern(lowerer, el.name); }); } - return pattern.elements.every((el) => { + return pattern.elements.nodes.every((el) => { if (el.name === undefined) return true; if (el.dotDotDotToken || !ts.isIdentifier(el.name)) return false; const prop = el.propertyName ?? el.name; @@ -3460,7 +3461,7 @@ function stdlibGlobalTokenDestructure( * of a token. */ value?: IrExpr; }[] = []; - for (const el of decl.name.elements) { + for (const el of decl.name.elements.nodes) { if (el.name === undefined) continue; if (el.dotDotDotToken) { lowerer.unsupported( @@ -3916,7 +3917,7 @@ function objectRestValue( const consumed = new Set(); let computedSibling = false; if (ts.isObjectBindingPattern(pattern)) { - for (const sib of pattern.elements) { + for (const sib of pattern.elements.nodes) { if (sib === el || sib.dotDotDotToken) continue; const key = sib.propertyName ?? sib.name; if (key && ts.isComputedPropertyName(key)) computedSibling = true; @@ -4291,7 +4292,7 @@ export function lowerVarDeclList( const isConst = (list.flags & ts.NodeFlags.Const) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0; if (!isConst && !isLet) { - const out = list.declarations.flatMap((decl) => { + const out = list.declarations.nodes.flatMap((decl) => { // `for (var {} = {}, [a] = xs; ...)`: pattern declarators ride the // declaration desugar (var names assign their hoisted slots). if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { @@ -4305,7 +4306,7 @@ export function lowerVarDeclList( return { kind: "block", body: out, loc: locOf(list) }; } const out: IrStmt[] = []; - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { // Destructuring produces nested initialization blocks; copying // their captured bindings still needs a distinct head-scope plan. @@ -4503,7 +4504,7 @@ export function lowerVarDecl( !isJsSourceFile(decl.getSourceFile()) && !decl .getSourceFile() - .statements.some( + .statements.nodes.some( (s) => ts.isImportDeclaration(s) || ts.isExportDeclaration(s) || @@ -4820,7 +4821,7 @@ export function lowerVarDecl( !decl.type && !hasJsTypeAnnotation(decl) && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.some( + decl.initializer.properties.nodes.some( (property) => ts.isMethodDeclaration(property) || ts.isAccessor(property) || @@ -4978,7 +4979,7 @@ export function lowerVarDecl( ) { let initExpr: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(initExpr)) initExpr = initExpr.expression; - if (ts.isArrayLiteralExpression(initExpr) && initExpr.elements.length === 0) { + if (ts.isArrayLiteralExpression(initExpr) && initExpr.elements.nodes.length === 0) { const t = lowerer.typeOf(decl.name); if (lowerer.checker.isArrayType(t)) { const elem = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]; @@ -5318,7 +5319,7 @@ function lowerVariableInitializer( if (expected?.kind === "dyn") return lowerDynObjectLiteral(lowerer, value); const previous = new Map(); - for (const property of value.properties) { + for (const property of value.properties.nodes) { if (!ts.isPropertyAssignment(property) || !ts.isComputedPropertyName(property.name)) continue; const keyNode = property.name.expression; const optional = @@ -5429,7 +5430,7 @@ function jsObjectBindingExpands( targetSymbol && aliases.has(targetSymbol) && ts.isObjectLiteralExpression(value) && - value.properties.some( + value.properties.nodes.some( (field) => !(ts.isPropertyAssignment(field) || ts.isShorthandPropertyAssignment(field)) || !(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) || @@ -5445,7 +5446,7 @@ function jsObjectBindingExpands( node.expression.name.text === "setPrototypeOf" && lowerer.isStdlibGlobal(node.expression.expression, "Object") ) { - const target = node.arguments[0]; + const target = node.arguments.nodes[0]; const sym = target && ts.isIdentifier(target) ? lowerer.resolveValueSymbol(target) : undefined; if (sym && aliases.has(sym)) expands = true; @@ -5555,7 +5556,7 @@ function immediateAbsenceGuard( if (!statement || !ts.isVariableStatement(statement)) return null; const container = statement.parent; const statements = - ts.isBlock(container) || ts.isSourceFile(container) ? container.statements : null; + ts.isBlock(container) || ts.isSourceFile(container) ? container.statements.nodes : null; if (!statements) return null; const index = statements.indexOf(statement); const next = index >= 0 ? statements[index + 1] : undefined; @@ -5614,7 +5615,7 @@ function falsyExitRuntimeOptionalLocals(lowerer: Lowerer, stmt: ts.IfStatement): if (stmt.elseStatement) return []; let tail = stmt.thenStatement; while (ts.isBlock(tail)) { - const last = tail.statements[tail.statements.length - 1]; + const last = tail.statements.nodes[tail.statements.nodes.length - 1]; if (!last) return []; tail = last; } @@ -5662,7 +5663,7 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt lowerer.scopes.push(new Map()); try { const cases: { test: IrExpr | null; body: IrStmt[] }[] = []; - for (const clause of stmt.caseBlock.clauses) { + for (const clause of stmt.caseBlock.clauses.nodes) { let test: IrExpr | null = null; if (ts.isCaseClause(clause)) { test = lowerer.lowerExpr(clause.expression); @@ -5678,7 +5679,10 @@ export function lowerSwitch(lowerer: Lowerer, stmt: ts.SwitchStatement): IrStmt test, body: lowerer.inCtl( "switch", - () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements)), + () => + withRuntimeOptionalScope(lowerer, () => + lowerer.lowerStmts(clause.statements.nodes), + ), labels, ), }); @@ -5780,7 +5784,7 @@ function lowerBranchSwitch( // Calls/assignments in a case test may mutate a captured discriminant // binding. The checker still narrows that binding as if it were the // snapshot. Keep the original tagged value in those case bodies. - const effectful = stmt.caseBlock.clauses.some( + const effectful = stmt.caseBlock.clauses.nodes.some( (clause) => ts.isCaseClause(clause) && !ts.isStringLiteral(clause.expression) && @@ -5798,13 +5802,13 @@ function lowerBranchSwitch( if (symbol) lowerer.unstableSwitchBindings.add(symbol); lowerer.scopes.push(new Map()); try { - const cases = stmt.caseBlock.clauses.map((clause) => ({ + const cases = stmt.caseBlock.clauses.nodes.map((clause) => ({ test: ts.isCaseClause(clause) ? compare(stableDisc, lowerer.lowerExpr(clause.expression), clause.expression) : null, body: lowerer.inCtl( "switch", - () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements)), + () => withRuntimeOptionalScope(lowerer, () => lowerer.lowerStmts(clause.statements.nodes)), labels, ), })); @@ -5895,12 +5899,12 @@ function isStrictDelete(node: ts.DeleteExpression): boolean { ts.isFunctionLike(parent) && parent.body && ts.isBlock(parent.body) && - hasUseStrictDirective(parent.body.statements) + hasUseStrictDirective(parent.body.statements.nodes) ) return true; if (ts.isSourceFile(parent)) { return ( - hasUseStrictDirective(parent.statements) || + hasUseStrictDirective(parent.statements.nodes) || (!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent))) ); } @@ -6179,7 +6183,7 @@ function lowerCjsExportStatement( if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; const sf = cjs.expr.getSourceFile(); let root: IrGlobal | undefined; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const whole = cjsExportAssignmentOf(stmt); if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); @@ -6270,7 +6274,7 @@ function lowerCjsExportStatement( ); } }; - for (const prop of table.properties) { + for (const prop of table.properties.nodes) { if (ts.isShorthandPropertyAssignment(prop)) { fenceMutable(prop.name as ts.Identifier); continue; // alias plumbing @@ -7333,7 +7337,7 @@ function droppedPureStdlibStatement(lowerer: Lowerer, expr: ts.Expression): bool if (!isStdlibMember(lowerer, proto) || !isStdlibMember(lowerer, callee)) return false; const root = stdlibGlobalNameOf(lowerer, strip(proto.expression)); if (root !== "Array" && root !== "String") return false; - return expr.arguments.every((a) => sideEffectFreeOptionValue(a)); + return expr.arguments.nodes.every((a) => sideEffectFreeOptionValue(a)); } if (ts.isPropertyAccessExpression(expr)) { return stdlibGlobalNameOf(lowerer, strip(expr.expression)) !== null; @@ -7516,8 +7520,8 @@ function destructuringAssignInto( // globals and strings alike). { const patternEmpty = ts.isObjectLiteralExpression(target) - ? target.properties.length === 0 - : target.elements.length === 0; + ? target.properties.nodes.length === 0 + : target.elements.nodes.length === 0; if (!patternEmpty && rhs !== null && isJsSourceFile(rhs.getSourceFile())) { const globalName = stdlibGlobalNameOf(lowerer, rhs); if (globalName !== null) { @@ -7573,7 +7577,7 @@ function destructuringAssignInto( ); } let firstProp: string | undefined; - const p0 = target.properties[0]; + const p0 = target.properties.nodes[0]; if (p0 !== undefined) { if (ts.isShorthandPropertyAssignment(p0)) firstProp = (p0.name as ts.Identifier).text; else if (ts.isPropertyAssignment(p0)) { @@ -7595,7 +7599,7 @@ function destructuringAssignInto( loc, }); } - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const piece = destructAssignPiece(lowerer, prop); const read: IrExpr = { kind: "dynKeyGet", @@ -7613,7 +7617,7 @@ function destructuringAssignInto( } return { stmts: out, value }; } - if (target.properties.length === 0) { + if (target.properties.nodes.length === 0) { // `({} = e)`: RequireObjectCoercible and nothing else. A source the // type system already proves non-nullish is a pure no-op past the // RHS's own evaluation; possibly-nullish unions would throw in JS @@ -7643,13 +7647,13 @@ function destructuringAssignInto( if (!lowerer.classes.get(objT.className)) lowerer.flushDeferredClass(objT.className); const info = lowerer.classes.get(objT.className); if (info) { - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const propLoc = locOf(prop); if (ts.isSpreadAssignment(prop)) { let restTo: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(restTo)) restTo = restTo.expression; const consumed = new Set(); - for (const p of target.properties) { + for (const p of target.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) consumed.add((p.name as ts.Identifier).text); else if (ts.isPropertyAssignment(p)) { const n = patternKeyNameOf(lowerer, p.name); @@ -7786,7 +7790,7 @@ function destructuringAssignInto( // properties keep the declaration path's named fences. Callers vetted // the source as a checker-typed string. if (init.type.kind === "string") { - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { const propLoc = locOf(prop); if (ts.isSpreadAssignment(prop)) { lowerer.unsupported( @@ -7861,7 +7865,7 @@ function destructuringAssignInto( const shape = lowerer.shapes.get(srcType.shapeId); if (!shape) throw new InternalCompilerError(`lowerer bug: destructuring unknown shape ${srcType.shapeId}`); - for (const prop of target.properties) { + for (const prop of target.properties.nodes) { // Shorthand `{ a }` assigns local a from field a; `{ a: x }` assigns // x from field a (a PropertyAssignment whose initializer is the // target — JS reuses the literal syntax with inverted roles). @@ -7881,7 +7885,7 @@ function destructuringAssignInto( ); } const consumed = new Set(); - for (const p of target.properties) { + for (const p of target.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) consumed.add((p.name as ts.Identifier).text); else if (ts.isPropertyAssignment(p)) { const n = patternKeyNameOf(lowerer, p.name); @@ -8227,7 +8231,7 @@ function lowerArrayAssignInto( // array divergence). The EMPTY pattern skips the split — GetIterator // + close, nothing observable — through the iterable early-return // below. Callers vetted the source as a checker-typed string. - if (srcType.kind === "string" && target.elements.length > 0) { + if (srcType.kind === "string" && target.elements.nodes.length > 0) { const charsT = arrayOf(STRING); const chars = lowerer.declareHiddenLocal("%dchars", charsT); out.push({ @@ -8245,13 +8249,13 @@ function lowerArrayAssignInto( // consumes exactly the elements the inner pattern alone would, so the // unwrap is exact. Identifier rest operands pack in the element loop // below. - let elements: readonly ts.Expression[] = target.elements; + let elements: readonly ts.Expression[] = target.elements.nodes; if ( elements.length === 1 && ts.isSpreadElement(elements[0]!) && ts.isArrayLiteralExpression(elements[0]!.expression) ) { - elements = elements[0]!.expression.elements; + elements = elements[0]!.expression.elements.nodes; } // The element-read builder per source kind; null when the source // cannot destructure. @@ -8573,7 +8577,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { while (ts.isParenthesizedExpression(src)) src = src.expression; if ( ts.isCallExpression(src) && - src.arguments.length === 0 && + src.arguments.nodes.length === 0 && !src.questionDotToken && ts.isPropertyAccessExpression(src.expression) && !src.expression.questionDotToken && @@ -8637,7 +8641,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { const constIdentBinding = ts.isVariableDeclarationList(stmt.initializer) && (stmt.initializer.flags & ts.NodeFlags.Const) !== 0 && - ts.isIdentifier(stmt.initializer.declarations[0]!.name); + ts.isIdentifier(stmt.initializer.declarations.nodes[0]!.name); if (constIdentBinding) { const call = directMatchAllCallOf(lowerer, src); if (call) return lowerForOfMatchAll(lowerer, stmt, call, null); @@ -8668,7 +8672,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { if ( ts.isCallExpression(e) && !e.questionDotToken && - e.arguments.length === 0 && + e.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(e.expression) && !e.expression.questionDotToken && e.expression.name.text === "values" && @@ -9181,7 +9185,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { const resourceLoop = (list.flags & ts.NodeFlags.Using) !== 0; const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one // `for (const [k, v] of pairs)` — a destructuring loop variable: the // element binds to a hidden per-iteration local and the pattern // desugars to reads of it at the top of the body, exactly the @@ -9362,7 +9366,7 @@ function lowerForOfStoredNumericIterator( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; + const decl = list ? list.declarations.nodes[0]! : null; if (decl && !ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); @@ -9499,13 +9503,13 @@ function directMatchAllCallOf(lowerer: Lowerer, e: ts.Expression): ts.CallExpres if ( ts.isCallExpression(src) && !src.questionDotToken && - src.arguments.length === 1 && + src.arguments.nodes.length === 1 && ts.isPropertyAccessExpression(src.expression) && !src.expression.questionDotToken && src.expression.name.text === "matchAll" && lowerer.isStdlibMember(src.expression) && lowerer.mapTypeOf(lowerer.typeOf(src.expression.expression))?.kind === "string" && - lowerer.mapTypeOf(lowerer.typeOf(src.arguments[0]!))?.kind === "regex" + lowerer.mapTypeOf(lowerer.typeOf(src.arguments.nodes[0]!))?.kind === "regex" ) { return src; } @@ -9537,7 +9541,7 @@ function lowerForOfMatchAll( stored: { rows: IrExpr; idxsLocalId: string } | null, ): IrStmt { const list = stmt.initializer as ts.VariableDeclarationList; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[0]!; const name = decl.name as ts.Identifier; const loc = locOf(stmt); const rowT = arrayOf(STRING); @@ -9551,7 +9555,7 @@ function lowerForOfMatchAll( if (call !== null) { const access = call.expression as ts.PropertyAccessExpression; const receiver = lowerer.lowerExpr(access.expression); - const re = lowerer.lowerExpr(call.arguments[0]!); + const re = lowerer.lowerExpr(call.arguments.nodes[0]!); const idxs = lowerer.declareHiddenLocal("%midxs", idxsT); idxsLocalId = idxs.id; prelude.push({ @@ -9708,7 +9712,7 @@ function lowerForOfClassIterator( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; + const decl = list ? list.declarations.nodes[0]! : null; if (decl && !ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); lowerer.scopes.push(new Map()); @@ -9940,7 +9944,7 @@ function lowerForOfDyn( } const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; - const decl = list.declarations[0]!; + const decl = list.declarations.nodes[0]!; if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { const tmp = lowerer.declareHiddenLocal("%destr", DYN); binds.push({ kind: "varDecl", localId: tmp.id, init: elemInit(), loc }); @@ -10123,7 +10127,7 @@ function lowerForOfString( lowerer.unsupported("SC1090", list, "'using' declarations (dispose-at-scope-exit semantics)"); } const isLet = list !== null && (list.flags & ts.NodeFlags.Let) !== 0; - const decl = list ? list.declarations[0]! : null; // the grammar allows exactly one + const decl = list ? list.declarations.nodes[0]! : null; // the grammar allows exactly one const loc = locOf(stmt); lowerer.scopes.push(new Map()); try { @@ -10489,7 +10493,7 @@ function lowerForInOverKeys( ); } const list = stmt.initializer; - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; @@ -10602,7 +10606,7 @@ function lowerForInArray( }, ]; } else { - const decl = stmt.initializer.declarations[0]!; + const decl = stmt.initializer.declarations.nodes[0]!; if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const isLet = (stmt.initializer.flags & ts.NodeFlags.Let) !== 0 || @@ -10713,7 +10717,7 @@ function lowerForAwaitStdin(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // threading it through a shared hoisted slot has no user yet. if (!isConst && !isLet) lowerer.unsupported("SC1030", list, "'var' loop bindings in 'for await' (use const)"); - const decl = list.declarations[0]!; // the grammar allows exactly one + const decl = list.declarations.nodes[0]!; // the grammar allows exactly one if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); const loc = locOf(stmt); const promiseT: IrType = { kind: "promise", inner: BYTES_U8 }; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 5f3d56114..7639e0974 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -275,7 +275,7 @@ function lowerStreamCallback( ): IrExpr { const loc = locOf(node); const { shapes, funcType } = lowerer.lambdaSignature(node); - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", node, `async stream '${which}' callbacks`); } if (funcType.params.length > fullTuple.length) { @@ -309,10 +309,10 @@ function lowerStreamCallback( const thisLocal = ts.isArrowFunction(node) ? lowerer.declareHiddenLocal("this", thisType) : lowerer.declareThis(thisType); - const { params, prologue } = lowerer.declareParams(node.parameters, shapes); + const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); let body: IrStmt[]; if (node.body !== undefined && ts.isBlock(node.body)) { - body = lowerer.lowerStmts(node.body.statements); + body = lowerer.lowerStmts(node.body.statements.nodes); } else if (node.body !== undefined) { // Bare-expression bodies (`read: () => this.push(null)`): the value // is discarded when the signature returns void — Node ignores @@ -627,7 +627,7 @@ function parseStreamOptions( } const tuples = cbTuples(lowerer); const present = new Map(); - for (const prop of optsLit?.properties ?? []) { + for (const prop of optsLit?.properties?.nodes ?? []) { const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name; const name = propName && ts.isIdentifier(propName) @@ -932,7 +932,7 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C const cls = info.def.name; if (!RUNTIME_STREAM_CLASSES.has(cls)) lowerer.unsupported("SC1090", expr, `constructing ${cls}`); const thisType: IrType = { kind: "object", className: cls }; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering( `new ${info.def.name.slice(1)} with ${args.length} arguments`, @@ -1410,15 +1410,15 @@ export function lowerStreamStaticCall( if ( member === "toWeb" && cls === "%Readable" && - call.arguments.length === 2 && + call.arguments.nodes.length === 2 && isJsSourceFile(call.getSourceFile()) && - ts.isObjectLiteralExpression(call.arguments[1]!) + ts.isObjectLiteralExpression(call.arguments.nodes[1]!) ) { - for (const p of call.arguments[1]!.properties) { + for (const p of call.arguments.nodes[1]!.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type") { const t = lowerer.typeOf(p.initializer); if (t.isStringLiteralType() && t.value !== "bytes") { - lowerer.lowerExpr(call.arguments[0]!); // evaluation order (the stream argument) + lowerer.lowerExpr(call.arguments.nodes[0]!); // evaluation order (the stream argument) return nodeThrowExpr( 1, "ERR_INVALID_ARG_VALUE", @@ -1436,7 +1436,7 @@ export function lowerStreamStaticCall( if (cls !== "%Readable") { lowerer.noLowering(`${cls.slice(1)}.from`, call, "Readable.from is the lowered form"); } - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 2) { lowerer.noLowering( `Readable.from with ${args.length} arguments`, @@ -1955,7 +1955,7 @@ function fromOptions( if (!ts.isObjectLiteralExpression(node)) return null; let flags = 0; let hwm: ts.Expression | null = null; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if ( !ts.isPropertyAssignment(prop) || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) @@ -2062,7 +2062,7 @@ function lowerStreamArg( argTypeThrow("body", PIPELINE_STAGE_EXPECTED); } } else if ( - (ts.isObjectLiteralExpression(node) && node.properties.length === 0) || + (ts.isObjectLiteralExpression(node) && node.properties.nodes.length === 0) || v.type.kind === "record" || v.type.kind === "string" || v.type.kind === "f64" || @@ -2193,7 +2193,7 @@ function eosOptions( "pass true or false", ); }; - for (const p of node.properties) { + for (const p of node.properties.nodes) { const key = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) ? p.name.text @@ -2236,7 +2236,7 @@ function pipelineOptions( node: ts.ObjectLiteralExpression, ): { end: boolean; signal: IrExpr | null } { const out = { end: true, signal: null as IrExpr | null }; - for (const p of node.properties) { + for (const p of node.properties.nodes) { const key = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && ts.isIdentifier(p.name) ? p.name.text @@ -2279,7 +2279,7 @@ export function lowerStreamModuleCall( loc: SrcLoc, ): IrExpr | null { if (bi.module === "stream/promises") { - const args = call.arguments; + const args = call.arguments.nodes; // finished(stream) → a pending void promise the terminal watcher // settles (rejected with the stream's error, or // ERR_STREAM_PREMATURE_CLOSE — the callback form's status, boxed @@ -2389,7 +2389,7 @@ export function lowerStreamModuleCall( return null; } if (bi.module === "stream/consumers") { - const args = call.arguments; + const args = call.arguments.nodes; // text/json/buffer/arrayBuffer(stream) → a pending promise the // accumulate-and-settle machinery resolves at the terminal point (the // utf8 decode / the parsed JSON dyn tree / the concatenated bytes / an @@ -2440,7 +2440,7 @@ export function lowerStreamModuleCall( return null; } if (bi.module !== "stream") return null; - const args = call.arguments; + const args = call.arguments.nodes; if (bi.member === "getDefaultHighWaterMark") { // Node 24: 65536 bytes / 16 objects — compile-time literals only. @@ -2657,7 +2657,7 @@ function lowerPipelineStages( for (let i = first; i < end; i++) { const fn = stageArgs[i] as ts.FunctionExpression | ts.ArrowFunction; const isLast = destFn && i === end - 1; - const isAsync = (fn.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); const isGen = ts.isFunctionExpression(fn) && fn.asteriskToken !== undefined; if (isLast && isGen) { // pipeline() pumps it into an object-mode PassThrough, whose @@ -2680,10 +2680,10 @@ function lowerPipelineStages( ); } const optionsAt = srcT === null ? 0 : 1; - if (fn.parameters.length > optionsAt + 1) { + if (fn.parameters.nodes.length > optionsAt + 1) { lowerer.noLowering( "pipeline stages with parameters after the options argument", - fn.parameters[optionsAt + 1]!, + fn.parameters.nodes[optionsAt + 1]!, "a stage takes the source and { signal }", ); } @@ -2693,7 +2693,7 @@ function lowerPipelineStages( if (params[optionsAt]!.kind !== "dyn") { lowerer.noLowering( "a pipeline stage's options argument with this type", - fn.parameters[optionsAt]!, + fn.parameters.nodes[optionsAt]!, "type it as { signal: AbortSignal }", ); } @@ -2897,7 +2897,7 @@ function lowerStageFunction( fn: ts.FunctionExpression | ts.ArrowFunction, srcT: IrType | null, ): { closure: IrExpr; out: IrType & ({ kind: "generator" } | { kind: "promise" }) } { - const param = srcT === null ? undefined : fn.parameters[0]; + const param = srcT === null ? undefined : fn.parameters.nodes[0]; const symbol = param && ts.isIdentifier(param.name) ? lowerer.checker.getSymbolAtLocation(param.name) @@ -2955,7 +2955,7 @@ function lowerStageFunction( * the wrong position, a single Readable or Writable (Duplex.from wraps * those), and functions, iterables or web streams have no lowering. */ function lowerStreamCompose(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; const duplex: IrType = { kind: "object", className: "%Duplex" }; if (args.some((a) => ts.isSpreadElement(a))) { lowerer.noLowering("compose with spread arguments", call, "pass the streams directly"); @@ -3048,9 +3048,9 @@ function lowerOperatorCallback( ); if ( (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && - node.parameters.length > maxParams + node.parameters.nodes.length > maxParams ) { - optionsParam(node.parameters[maxParams]!); + optionsParam(node.parameters.nodes[maxParams]!); } const v = lowerer.lowerExpr(node); if (v.type.kind === "dyn") return v; @@ -3135,7 +3135,7 @@ function lowerStreamOperator( member: string, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const recv = lowerer.lowerExpr(access.expression); const readable: IrType = { kind: "object", className: "%Readable" }; const opts = (i: number): IrExpr => @@ -3267,7 +3267,7 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc "{ end: } inline is the supported form", ); } - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name) || prop.name.text !== "end") { lowerer.noLowering("a pipe option other than 'end'", prop); } @@ -3292,7 +3292,7 @@ export function lowerStreamMethodCall( const sides = streamSidesOf(lowerer, info); if (!sides) return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const readableSide = sides === "r" || sides === "rw"; const writableSide = sides === "w" || sides === "rw"; const cls = info.def.name.slice(1); diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index c31e674af..9677de6ff 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -173,7 +173,7 @@ function lowerTestOptions( ); } const out = noOptions(); - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { if ( !(ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) && !ts.isShorthandPropertyAssignment(prop) @@ -353,7 +353,7 @@ function splitTestArgs( expr: ts.CallExpression, what: string, ): { nameNode?: ts.Expression; optionsNode?: ts.Expression; fnNode?: ts.Expression } { - const args = expr.arguments; + const args = expr.arguments.nodes; const isFn = (a: ts.Expression): boolean => lowerer.checker.getCallSignatures(lowerer.typeOf(a)).length > 0; const isOptions = (a: ts.Expression): boolean => ts.isObjectLiteralExpression(a); @@ -589,7 +589,7 @@ function lowerHook( loc: SrcLoc, ): IrExpr { requireStatementPosition(lowerer, expr, what); - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length !== 1) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -726,7 +726,7 @@ function lowerSnapshotAssert( return varRef(local.id, type, loc); }; // Arguments evaluate in order before the assertion serializes, as in Node. - const valueNode = call.arguments[0]; + const valueNode = call.arguments.nodes[0]; const input = slot( "%snapshotInput", DYN, @@ -734,7 +734,7 @@ function lowerSnapshotAssert( ); let path: IrExpr | null = null; if (member === "fileSnapshot") { - const pathNode = call.arguments[1]; + const pathNode = call.arguments.nodes[1]; path = slot( "%snapshotPath", STRING, @@ -742,11 +742,11 @@ function lowerSnapshotAssert( ); } const serializers = - snapshotSerializers(lowerer, call.arguments[member === "snapshot" ? 1 : 2], what)?.map((fn) => - slot("%snapshotSerializer", DYN, fn), + snapshotSerializers(lowerer, call.arguments.nodes[member === "snapshot" ? 1 : 2], what)?.map( + (fn) => slot("%snapshotSerializer", DYN, fn), ) ?? null; - if (call.arguments.length > (member === "snapshot" ? 2 : 3)) { - lowerer.noLowering(`${what} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > (member === "snapshot" ? 2 : 3)) { + lowerer.noLowering(`${what} with ${call.arguments.nodes.length} arguments`, call); } const text: IrExpr = { kind: "toString", @@ -813,7 +813,7 @@ function snapshotSerializers( lowerer.noLowering(`${what} options that are not an object literal`, options, hint); } let fns: IrExpr[] | null = null; - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; if ( name !== "serializers" || @@ -825,7 +825,7 @@ function snapshotSerializers( if (!ts.isArrayLiteralExpression(list)) { lowerer.noLowering(`${what} with a non-literal serializers list`, list, hint); } - fns = list.elements.map((el) => { + fns = list.elements.nodes.map((el) => { if (ts.isSpreadElement(el)) lowerer.noLowering(`${what} with a spread serializer`, el, hint); return lowerer.lowerExprExpecting(el, DYN); }); @@ -1023,7 +1023,7 @@ export function lowerTestMethodCall( // The TestContext receiver surface. if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "testCtx") return null; if (!lowerer.isStdlibMember(access)) return null; - const args = call.arguments; + const args = call.arguments.nodes; if (member === "test") { const receiver = lowerer.lowerExpr(access.expression); return lowerRegistration(lowerer, call, "t.test(...)", loc, 0, receiver); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index c2a54dfc0..740ff66f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -284,14 +284,14 @@ export const TIMER_MODULE_MEMBERS: ReadonlySet = new Set([ * path. Null when the argument might be a real handle. */ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr | null { const noop: IrExpr = { kind: "libCall", fn: "timers.clearNoop", args: [], type: VOID, loc }; - if (expr.arguments.length === 0) return noop; - if (expr.arguments.length !== 1) return null; - let arg = expr.arguments[0]!; + if (expr.arguments.nodes.length === 0) return noop; + if (expr.arguments.nodes.length !== 1) return null; + let arg = expr.arguments.nodes[0]!; // `{} as never` / parenthesized spellings: the cast changes no value. while (ts.isAsExpression(arg) || ts.isTypeAssertion(arg) || ts.isParenthesizedExpression(arg)) arg = arg.expression; if (arg.kind === ts.SyntaxKind.NullKeyword) return noop; - if (ts.isObjectLiteralExpression(arg) && arg.properties.length === 0) return noop; + if (ts.isObjectLiteralExpression(arg) && arg.properties.nodes.length === 0) return noop; if (ts.isIdentifier(arg)) { const t = lowerer.mapTypeOf(lowerer.typeOf(arg)); if (t && (t.kind === "nullT" || t.kind === "undefinedT")) return noop; @@ -369,7 +369,7 @@ function lowerTimerCall( // Delayed timers share callback adaptation and the 1ms default. Trailing // arguments are captured now and delivered when the callback fires. if (member === "setTimeout" || member === "setInterval") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( `${member} with 0 arguments`, expr, @@ -377,19 +377,25 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 2 - ? makeTimerArgsThunk(lowerer, expr.arguments[0]!, expr.arguments.slice(2), member, loc) + expr.arguments.nodes.length > 2 + ? makeTimerArgsThunk( + lowerer, + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(2), + member, + loc, + ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); // An omitted delay is NaN, Node's undefined: the loop clamps it to // 1ms, and mock.timers queues it to run at the next tick. const ms: IrExpr = - expr.arguments.length >= 2 - ? lowerer.lowerExpr(expr.arguments[1]!) + expr.arguments.nodes.length >= 2 + ? lowerer.lowerExpr(expr.arguments.nodes[1]!) : { kind: "numLit", value: NaN, type: F64, loc }; if (member === "setInterval") { return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc }; @@ -410,10 +416,10 @@ function lowerTimerCall( if (member === "clearTimeout") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearTimeout with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr); } - let handle = lowerer.lowerExpr(expr.arguments[0]!); + let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); const ref = varRef(stored.id, DYN, loc); @@ -423,7 +429,7 @@ function lowerTimerCall( result: { kind: "ternary", cond: { kind: "dynTest", test: "number", value: ref, type: BOOL, loc }, - then: lowerer.coerceInto(expr.arguments[0]!, ref, F64), + then: lowerer.coerceInto(expr.arguments.nodes[0]!, ref, F64), else_: numLit(0, loc), type: F64, loc, @@ -436,7 +442,7 @@ function lowerTimerCall( if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setTimeout returned, or undefined or null", ); } @@ -445,14 +451,14 @@ function lowerTimerCall( if (member === "clearInterval") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearInterval with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearInterval of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setInterval returned, or undefined or null", ); } @@ -462,7 +468,7 @@ function lowerTimerCall( // the f64 immediate id (its own space — clearTimeout of an Immediate // no-ops, like Node); the callback adapts like setTimeout's. if (member === "setImmediate") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( "setImmediate with 0 arguments", expr, @@ -470,18 +476,18 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 1 + expr.arguments.nodes.length > 1 ? makeTimerArgsThunk( lowerer, - expr.arguments[0]!, - expr.arguments.slice(1), + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(1), "setImmediate", loc, ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); return { kind: "libCall", fn: "timers.setImmediate", args: [cb], type: F64, loc }; @@ -489,14 +495,14 @@ function lowerTimerCall( if (member === "clearImmediate") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearImmediate with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearImmediate of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Immediate setImmediate returned, or undefined or null", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-url.ts b/packages/compiler/src/frontend/lowering/lower-url.ts index f12d6b267..ee7e4a0af 100644 --- a/packages/compiler/src/frontend/lowering/lower-url.ts +++ b/packages/compiler/src/frontend/lowering/lower-url.ts @@ -35,7 +35,7 @@ function lowerUrlOperation( expr: ts.NewExpression | ts.CallExpression, operation: "new" | "canParse" | "parse", ): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const loc = locOf(expr); const resultType = operation === "new" ? URL_T : operation === "canParse" ? BOOL : DYN; const checkedResult = (value: IrExpr): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1873ac361..b0985e36d 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -677,7 +677,7 @@ export interface LowerStats { export function boundIdentifiersOf(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { // Elisions: OmittedExpression in 5.9.3, a NAMELESS BindingElement in 7 // (the parity battery's pinned finding 2) — both spell "no binding". if (ts.isOmittedExpression(el) || el.name === undefined) continue; @@ -694,7 +694,7 @@ export function countBodyStatements(root: ts.Node): number { let n = 0; const visit = (node: ts.Node): void => { if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) { - n += node.statements.length; + n += node.statements.nodes.length; } else if ( (ts.isIfStatement(node) || ts.isForStatement(node) || @@ -1185,7 +1185,7 @@ export function staticImportNamespaceType( } else break; } if (!ts.isCallExpression(e) || e.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - const arg = e.arguments[0]; + const arg = e.arguments.nodes[0]; if (!arg || !ts.isStringLiteralLike(arg)) return null; const builtin = canonicalBuiltinModule(arg.text); let moduleId: string | null = builtin === null ? null : `builtin:${builtin}`; @@ -1502,15 +1502,15 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { !isJsSourceFile(decl.getSourceFile()) ) return null; - const hasRest = decl.parameters.some((p) => p.dotDotDotToken !== undefined); + const hasRest = decl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined); if ( !hasRest && (ts.isArrowFunction(decl) || !bodyReadsArguments(decl) || - (decl.parameters.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) + (decl.parameters.nodes.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) ) return null; - const shapes = paramShapes(lowerer, decl.parameters); + const shapes = paramShapes(lowerer, decl.parameters.nodes); const retType = lowerer.checker.getReturnTypeOfSignature(sigs[0]!); const ret = retType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined) @@ -1546,7 +1546,7 @@ function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | (() => { const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!); const declParams = - sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; + sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if (declParams !== undefined && declParams.length !== sigs[0]!.getParameters().length) return false; // tsgo never synthesizes the `arguments` pseudo-rest into the @@ -3029,10 +3029,10 @@ export class Lowerer { if ( !this.classes.has(this.classNamer(decl)) && !this.collectingExprClasses.has(decl) && - decl.heritageClauses?.some( + decl.heritageClauses?.nodes.some( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword && - clause.types.some((base) => { + clause.types.nodes.some((base) => { const expression = base.expression; if (ts.isCallExpression(expression)) return true; if (ts.isIdentifier(expression) && this.isStdlibGlobal(expression, expression.text)) @@ -3071,7 +3071,7 @@ export class Lowerer { continue; if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break; if ( - parent.typeParameters?.length || + parent.typeParameters?.nodes.length || (implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent)) ) return null; @@ -3405,7 +3405,7 @@ export class Lowerer { } if (ts.isCallExpression(value)) { if (value.expression.kind === ts.SyntaxKind.ImportKeyword) { - const spec = value.arguments[0]; + const spec = value.arguments.nodes[0]; return spec !== undefined && ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text) @@ -3415,9 +3415,9 @@ export class Lowerer { if ( ts.isIdentifier(value.expression) && value.expression.text === "require" && - value.arguments.length === 1 + value.arguments.nodes.length === 1 ) { - const spec = value.arguments[0]!; + const spec = value.arguments.nodes[0]!; if (ts.isStringLiteralLike(spec) && this.externalTypes.has(spec.text)) return spec.text; } return this.externalTypeSpecifierOf(value.expression); @@ -4113,7 +4113,7 @@ export class Lowerer { * frame either). Prepended to the entry's %init. */ punycodeLinkWarning(): IrStmt[] { const imports = this.moduleOrder.some((sf) => - sf.statements.some( + sf.statements.nodes.some( (stmt) => ((ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) || (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) && @@ -4259,7 +4259,7 @@ export class Lowerer { const declared = this.mapTypeOf(this.typeOf(node.name)); if (declared?.kind !== "record") return undefined; let widened: IrType = declared; - for (const property of init.properties) { + for (const property of init.properties.nodes) { const value = ts.isPropertyAssignment(property) ? property.initializer : ts.isShorthandPropertyAssignment(property) @@ -4285,9 +4285,9 @@ export class Lowerer { * initializer as the matching absence probe. */ analyzeGuardedRecordReadGlobals(parts: FileParts[]): void { for (const { sf } of parts) { - for (const statement of sf.statements) { + for (const statement of sf.statements.nodes) { if (!ts.isVariableStatement(statement)) continue; - for (const decl of statement.declarationList.declarations) { + for (const decl of statement.declarationList.declarations.nodes) { if (!ts.isIdentifier(decl.name) || !isGuardedRecordRead(this, decl)) continue; const global = this.globalOf(decl.name); const symbol = this.resolveValueSymbol(decl.name); @@ -4429,7 +4429,7 @@ export class Lowerer { (ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name ? symbolOf(fn.name) : null; - const index = (fn as ts.FunctionLikeDeclaration).parameters.indexOf(parameter); + const index = (fn as ts.FunctionLikeDeclaration).parameters.nodes.indexOf(parameter); return (symbol && signatureBySymbol.get(symbol)?.params[index]?.type) ?? null; }; const returnTypeOf = (fn: ts.FunctionDeclaration | ts.MethodDeclaration): IrType | null => { @@ -4638,12 +4638,12 @@ export class Lowerer { if (isJsSourceFile(sf) || sf.isDeclarationFile) continue; ts.walkPreorder(sf, (node) => { if (!ts.isClassDeclaration(node)) return undefined; - const genericClass = (node.typeParameters?.length ?? 0) > 0; - for (const member of node.members) + const genericClass = (node.typeParameters?.nodes.length ?? 0) > 0; + for (const member of node.members.nodes) if ( ts.isMethodDeclaration(member) && member.body && - (genericClass || (member.typeParameters?.length ?? 0) > 0) + (genericClass || (member.typeParameters?.nodes.length ?? 0) > 0) ) genericClassMethods.push(member); return undefined; @@ -4718,7 +4718,7 @@ export class Lowerer { !this.isStdlibGlobal(init.expression.expression, "Object") ) return false; - const object = init.arguments[0]; + const object = init.arguments.nodes[0]; const objectType = object ? this.mapTypeOf(this.typeOf(object)) : null; return objectType?.kind === "dyn"; }; @@ -4803,7 +4803,7 @@ export class Lowerer { if (ts.isCallExpression(e) && this.runtimeOptionalReduceTypes.has(e)) return true; if ( ts.isCallExpression(e) && - e.arguments.length === 0 && + e.arguments.nodes.length === 0 && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "stringify" && this.isStdlibGlobal(e.expression.expression, "JSON") @@ -4971,7 +4971,7 @@ export class Lowerer { // A fresh literal carries the fields its values can leave undefined. if (ts.isObjectLiteralExpression(e)) { const out = spreadFieldsOf(e); - for (const prop of e.properties) { + for (const prop of e.properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue; const name = prop.name; if (!(ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))) @@ -5006,7 +5006,7 @@ export class Lowerer { * the properties a later member writes explicitly. */ const spreadFieldsOf = (object: ts.ObjectLiteralExpression): Set => { const out = new Set(); - for (const prop of object.properties) { + for (const prop of object.properties.nodes) { if (ts.isSpreadAssignment(prop)) { const fields = fieldsOfValue(prop.expression); if (fields !== undefined) for (const field of fields) out.add(field); @@ -5025,7 +5025,7 @@ export class Lowerer { const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; let changed = false; - name.elements.forEach((el) => { + name.elements.nodes.forEach((el) => { if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); @@ -5066,7 +5066,7 @@ export class Lowerer { const indexValue = shape?.indexValue; if (!shape || shape.tuple || !indexValue || indexValue.kind === "dyn") return false; let changed = false; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) continue; const key = el.propertyName ?? el.name; if (!ts.isIdentifier(key) && !ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) @@ -5211,7 +5211,7 @@ export class Lowerer { const promoteNode = (fn: ts.FunctionLikeDeclaration): void => { for (const parameterIndex of parameterIndices) { if (parameterIndex === undefined) continue; - const parameter = fn.parameters[parameterIndex]; + const parameter = fn.parameters.nodes[parameterIndex]; if (!parameter) continue; const symbol = ts.isIdentifier(parameter.name) ? symbolOf(parameter.name) : null; const checkerType = this.typeOf(parameter.name); @@ -5325,7 +5325,9 @@ export class Lowerer { ) ownerSymbol = symbolOf(owner.name); const ownerSig = ownerSymbol ? signatureBySymbol.get(ownerSymbol) : undefined; - const ownerIndex = ts.isFunctionLike(owner) ? owner.parameters.indexOf(declaration) : -1; + const ownerIndex = ts.isFunctionLike(owner) + ? owner.parameters.nodes.indexOf(declaration) + : -1; const slot = ownerIndex >= 0 ? ownerSig?.params[ownerIndex] : undefined; if (slot && !typeEquals(slot.type, promoted)) { slot.type = promoted; @@ -5347,7 +5349,7 @@ export class Lowerer { if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) return false; let changed = false; for (const parameterIndex of parameterIndices) { - const parameter = fn.parameters[parameterIndex]; + const parameter = fn.parameters.nodes[parameterIndex]; if (!parameter) continue; const checkerType = this.typeOf(parameter.name); if ((checkerType.flags & ts.TypeFlags.TypeParameter) !== 0) continue; @@ -5383,7 +5385,7 @@ export class Lowerer { const fn = peel(callback); let param: IrType | null; if (ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) { - const parameter = fn.parameters[index]; + const parameter = fn.parameters.nodes[index]; if (!parameter?.type || parameter.dotDotDotToken) return true; const checkerType = this.typeOf(parameter.name); if ((checkerType.flags & ts.TypeFlags.TypeParameter) !== 0) return true; @@ -5565,12 +5567,12 @@ export class Lowerer { else if (ts.isReturnStatement(node) && node.expression && dynamicValue(node.expression)) target(contextualOrNull(node.expression)); else if (ts.isCallExpression(node) || ts.isNewExpression(node)) { - for (const arg of node.arguments ?? []) + for (const arg of node.arguments?.nodes ?? []) if (!ts.isSpreadElement(arg) && dynamicValue(arg)) target(contextualOrNull(arg)); } else if (ts.isPropertyAssignment(node) && dynamicValue(node.initializer)) target(contextualOrNull(node.initializer)); else if (ts.isArrayLiteralExpression(node)) { - for (const element of node.elements) + for (const element of node.elements.nodes) if ( !ts.isSpreadElement(element) && !ts.isOmittedExpression(element) && @@ -5796,7 +5798,7 @@ export class Lowerer { ts.isObjectLiteralExpression(peel(node.initializer)) ) { const object = peel(node.initializer) as ts.ObjectLiteralExpression; - for (const prop of object.properties) { + for (const prop of object.properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue; const name = prop.name; @@ -5892,9 +5894,9 @@ export class Lowerer { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { const callbackIndices = hofCallbackIndices( node.expression.name.text, - node.arguments.length >= 2, + node.arguments.nodes.length >= 2, ); - const callback = node.arguments[0]; + const callback = node.arguments.nodes[0]; if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) { const receiverNode = node.expression.expression; const receiverTs = this.typeOf(receiverNode); @@ -5957,7 +5959,7 @@ export class Lowerer { changed = true; if ( (method === "reduce" || method === "reduceRight") && - ((!elementsPresent && node.arguments.length < 2) || + ((!elementsPresent && node.arguments.nodes.length < 2) || callbackReturnsOptional(callback)) ) { if (promoteHofCallback(callback, [0, 1])) changed = true; @@ -5991,7 +5993,7 @@ export class Lowerer { if (!value || !(ts.isArrowFunction(value) || ts.isFunctionExpression(value))) return; // Optional, defaulted and rest parameters keep their own ABI. if ( - value.parameters.some( + value.parameters.nodes.some( (parameter) => parameter.questionToken !== undefined || parameter.initializer !== undefined || @@ -5999,10 +6001,10 @@ export class Lowerer { ) ) return; - const indices = node.arguments.flatMap((arg, i) => + const indices = node.arguments.nodes.flatMap((arg, i) => !ts.isSpreadElement(arg) && - value.parameters[i] !== undefined && - !value.parameters[i]!.dotDotDotToken && + value.parameters.nodes[i] !== undefined && + !value.parameters.nodes[i]!.dotDotDotToken && mayBeOptional(arg) ? [i] : [], @@ -6019,10 +6021,10 @@ export class Lowerer { if ( callee && isJsSourceFile(callee.getSourceFile()) && - !node.arguments.some(ts.isSpreadElement) + !node.arguments.nodes.some(ts.isSpreadElement) ) { for (let i = 0; i < sig.params.length; i++) { - const arg = node.arguments[i]; + const arg = node.arguments.nodes[i]; if ( arg && (this.typeOf(arg).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0 @@ -6034,7 +6036,7 @@ export class Lowerer { const widened = addUndefined(shape.type); if (typeEquals(shape.type, widened)) continue; shape.type = widened; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note("parameter", parameter, node, "(omitted or undefined argument)"); if (parameter) { for (const bound of boundIdentifiersOf(parameter.name)) { @@ -6054,7 +6056,7 @@ export class Lowerer { (shape) => shape.mode === "rest" || shape.mode === "dynRest" || shape.mode === "arguments", ); - node.arguments.forEach((arg, argumentIndex) => { + node.arguments.nodes.forEach((arg, argumentIndex) => { const i = restAt >= 0 && argumentIndex >= restAt ? restAt : argumentIndex; const shape = sig.params[i]; const callbackSlot = shape?.type; @@ -6074,7 +6076,7 @@ export class Lowerer { const fields = fieldsOfValue(arg)!; for (const target of familyBySymbol.get(symbol) ?? [symbol]) { const parameterShape = signatureBySymbol.get(target)?.params[i]; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; if (!parameterShape || parameterShape.type.kind !== "record") continue; if (!parameter || !ts.isIdentifier(parameter.name)) continue; const bound = symbolOf(parameter.name); @@ -6123,7 +6125,7 @@ export class Lowerer { const widened = arrayOf(addUndefined(rest.type.elem)); if (typeEquals(rest.type, widened)) continue; rest.type = widened; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note("rest-parameter", parameter, arg); if (parameter && ts.isIdentifier(parameter.name)) { const bound = symbolOf(parameter.name); @@ -6148,7 +6150,7 @@ export class Lowerer { optionalParams.set(target, set); if (set.size !== before) { changed = true; - const parameter = functionDeclBySymbol.get(target)?.parameters[i]; + const parameter = functionDeclBySymbol.get(target)?.parameters?.nodes[i]; provenance?.note( "parameter", parameter, @@ -6358,7 +6360,7 @@ export class Lowerer { for (const [symbol, params] of optionalParams) { const decl = functionDeclBySymbol.get(symbol); for (const index of [...params]) { - const name = decl?.parameters[index]?.name; + const name = decl?.parameters?.nodes[index]?.name; const param = name && ts.isIdentifier(name) ? symbolOf(name) : null; if (param && nanCoded.nanCodedBinding(param)) params.delete(index); } @@ -6610,8 +6612,8 @@ export class Lowerer { continue; const symbol = callableSymbolOf(node.expression); const sig = symbol ? signatureBySymbol.get(symbol) : undefined; - if (!sig || node.arguments.some(ts.isSpreadElement)) continue; - node.arguments.forEach((arg, i) => { + if (!sig || node.arguments.nodes.some(ts.isSpreadElement)) continue; + node.arguments.nodes.forEach((arg, i) => { let e = arg; while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isNonNullExpression(e) || !isIndexedRead(e.expression)) return; @@ -7025,7 +7027,9 @@ export class Lowerer { fp.fnDecls .filter((decl) => decl.body !== undefined && decl.typeParameters === undefined) .flatMap((decl) => - decl.parameters.flatMap((param) => (param.initializer ? [param.initializer] : [])), + decl.parameters.nodes.flatMap((param) => + param.initializer ? [param.initializer] : [], + ), ), ), ); @@ -7065,10 +7069,10 @@ export class Lowerer { const bodyRoots = (...roots: (ts.Node | undefined | null)[]): ts.Node[] => roots.filter((root) => root !== undefined && root !== null); const functionRoots = (decl: ts.FunctionLikeDeclaration): ts.Node[] => - bodyRoots(...decl.parameters.map((param) => param.initializer), decl.body); + bodyRoots(...decl.parameters.nodes.map((param) => param.initializer), decl.body); const classCtorRoots = (info: ClassInfo): ts.Node[] => bodyRoots( - ...(info.ctor?.parameters ?? []).map((param) => param.initializer), + ...(info.ctor?.parameters?.nodes ?? []).map((param) => param.initializer), info.ctor?.body, ...info.fieldOrder.map((field) => field.initializer), ); @@ -9544,13 +9548,13 @@ export class Lowerer { // belong to the array the program keeps, rather than a typed snapshot. if ( ts.isArrayLiteralExpression(x) && - x.elements.every( + x.elements.nodes.every( (element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element), ) ) { return { kind: "dynArrLit", - elems: x.elements.map((element) => this.lowerExprExpecting(element, DYN)), + elems: x.elements.nodes.map((element) => this.lowerExprExpecting(element, DYN)), type: DYN, loc: locOf(x), }; @@ -9596,7 +9600,7 @@ export class Lowerer { // a unit-only-element array — a checker type that degraded to one // (`[]`-flavored inference over a populated literal) carries no // element information. - const nonUnitElems = x.elements.some( + const nonUnitElems = x.elements.nodes.some( (el) => !ts.isOmittedExpression(el) && el.kind !== ts.SyntaxKind.NullKeyword && @@ -9608,20 +9612,20 @@ export class Lowerer { (a.kind === "array" && !(nonUnitElems && this.unitOnlyElem(a.elem))) || (a.kind === "record" && !!this.shapes.get(a.shapeId)?.tuple && - this.shapes.get(a.shapeId)!.fields.length === x.elements.length && - !x.elements.some(ts.isSpreadElement)), + this.shapes.get(a.shapeId)!.fields.length === x.elements.nodes.length && + !x.elements.nodes.some(ts.isSpreadElement)), ); let arm = arms.length === 1 ? arms[0] : undefined; if ( !arm && arms.length > 1 && - x.elements.length > 0 && - x.elements.every((el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) + x.elements.nodes.length > 0 && + x.elements.nodes.every((el) => !ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) ) { const ownType = this.mapTypeOf(this.typeOf(x)); arm = arms.find((candidate) => ownType !== null && typeEquals(candidate, ownType)); if (!arm) { - const elements = x.elements.map((el) => tryLowerExpression(this, el)); + const elements = x.elements.nodes.map((el) => tryLowerExpression(this, el)); arm = arms.find( (candidate) => candidate.kind === "array" && @@ -9661,8 +9665,8 @@ export class Lowerer { if ( !ts.isNewExpression(value) || !ts.isIdentifier(value.expression) || - (value.arguments?.length ?? 0) !== 0 || - (value.typeArguments?.length ?? 0) !== 0 + (value.arguments?.nodes.length ?? 0) !== 0 || + (value.typeArguments?.nodes.length ?? 0) !== 0 ) return null; const symbol = this.resolveValueSymbol(value.expression); @@ -10236,7 +10240,7 @@ export class Lowerer { const typeNodes: ts.Node[] = []; const symbolRoots: ts.Node[] = []; for (const decl of decls) { - for (const member of decl.members) { + for (const member of decl.members.nodes) { if ( (ts.isConstructorDeclaration(member) || ts.isMethodDeclaration(member) || @@ -10244,18 +10248,18 @@ export class Lowerer { ts.isSetAccessor(member)) && (!ts.isMethodDeclaration(member) || member.typeParameters === undefined) ) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) defaultTypeNodes.push(param.initializer); } } } if (!isJsSourceFile(decl.getSourceFile())) continue; - for (const member of decl.members) { + for (const member of decl.members.nodes) { if (ts.isConstructorDeclaration(member)) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) typeNodes.push(param.initializer); } - for (const stmt of member.body?.statements ?? []) { + for (const stmt of member.body?.statements?.nodes ?? []) { if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue; if (stmt.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue; @@ -10277,7 +10281,7 @@ export class Lowerer { ts.isGetAccessor(member) || ts.isSetAccessor(member) ) { - for (const param of member.parameters) { + for (const param of member.parameters.nodes) { if (param.initializer) typeNodes.push(param.initializer); } // npm-static implicit-any classification resolves body parameter @@ -10528,7 +10532,7 @@ export class Lowerer { includeAbstract = false, ): Generator<{ mName: string; member: ts.MethodDeclaration | ts.AccessorDeclaration }> { if (!info.decl) return; // builtin error classes: runtime-provided bodies - for (const member of info.decl.members) { + for (const member of info.decl.members.nodes) { const fnLike = ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member) ? member @@ -10756,7 +10760,7 @@ export class Lowerer { if ( abi.kind === "dyn" && ts.isObjectBindingPattern(decl.name) && - decl.name.elements.length === 0 + decl.name.elements.nodes.length === 0 ) { prologue.push({ kind: "exprStmt", @@ -11036,7 +11040,7 @@ export class Lowerer { vd && ts.isParameter(vd) && ts.isIdentifier(vd.name) && - vd.modifiers?.some( + vd.modifiers?.nodes.some( (m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index b84672406..4270457d5 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -213,15 +213,15 @@ function adjacentDeclarations(a: ts.VariableDeclaration, b: ts.VariableDeclarati const lb = b.parent; if (!ts.isVariableDeclarationList(la) || !ts.isVariableDeclarationList(lb)) return false; if (la === lb) { - const ia = la.declarations.indexOf(a); - const ib = la.declarations.indexOf(b); + const ia = la.declarations.nodes.indexOf(a); + const ib = la.declarations.nodes.indexOf(b); return Math.abs(ia - ib) === 1; } const sa = la.parent; const sb = lb.parent; if (!ts.isVariableStatement(sa) || !ts.isVariableStatement(sb) || sa.parent !== sb.parent) return false; - if (la.declarations.length !== 1 || lb.declarations.length !== 1) return false; + if (la.declarations.nodes.length !== 1 || lb.declarations.nodes.length !== 1) return false; const list = sa.parent; if ( !ts.isBlock(list) && @@ -230,7 +230,7 @@ function adjacentDeclarations(a: ts.VariableDeclaration, b: ts.VariableDeclarati !ts.isDefaultClause(list) ) return false; - const statements = (list as { statements: ts.NodeArray }).statements; + const statements = (list as { statements: ts.NodeArray }).statements.nodes; return Math.abs(statements.indexOf(sa) - statements.indexOf(sb)) === 1; } @@ -609,8 +609,8 @@ export class NanCodedReads { const na = this.fixedLength(sa); const nb = this.fixedLength(sb); if (!na || !nb) return false; - const la = na.arguments![0]!; - const lb = nb.arguments![0]!; + const la = na.arguments!.nodes[0]!; + const lb = nb.arguments!.nodes[0]!; // The same literal length. const literal = (e: ts.Expression): number | null => { const x = peelTransparent(e); @@ -695,10 +695,10 @@ export class NanCodedReads { !ts.isIdentifier(init.expression) || !TYPED_ARRAYS.has(init.expression.text) || !this.host.isStdlibGlobal(init.expression, init.expression.text) || - init.typeArguments || - init.arguments?.length !== 1 || - ts.isSpreadElement(init.arguments[0]!) || - !this.host.isNumber(init.arguments[0]!) + init.typeArguments?.nodes || + init.arguments?.nodes.length !== 1 || + ts.isSpreadElement(init.arguments.nodes[0]!) || + !this.host.isNumber(init.arguments.nodes[0]!) ) return null; const name = decl.name; @@ -753,7 +753,7 @@ export class NanCodedReads { this.fnEligibility.set(symbol, null); const decl = this.host.declarationOf(symbol); if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl))) return null; - if (!decl.body || decl.asteriskToken || decl.typeParameters || !decl.name) return null; + if (!decl.body || decl.asteriskToken || decl.typeParameters?.nodes || !decl.name) return null; if (!ts.isIdentifier(decl.name)) return null; const sf = decl.getSourceFile(); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; @@ -762,7 +762,7 @@ export class NanCodedReads { return null; if (ts.isMethodDeclaration(decl)) { const cls = decl.parent; - if (!ts.isClassDeclaration(cls) || (cls.heritageClauses?.length ?? 0) > 0) return null; + if (!ts.isClassDeclaration(cls) || (cls.heritageClauses?.nodes.length ?? 0) > 0) return null; if (this.host.methodOverridden(symbol)) return null; } // Sloppy-mode `arguments` aliases parameters. @@ -806,7 +806,7 @@ export class NanCodedReads { !ts.isCallExpression(call) || call.expression !== callee || call.questionDotToken || - call.arguments.some((a) => ts.isSpreadElement(a)) + call.arguments.nodes.some((a) => ts.isSpreadElement(a)) ) { ok = false; break; @@ -863,12 +863,12 @@ export class NanCodedReads { if (!ts.isFunctionDeclaration(fn) && !ts.isMethodDeclaration(fn)) return null; const fnSymbol = fn.name ? this.symbolOf(fn.name) : null; if (!fnSymbol || !this.eligibleFunction(fnSymbol)) return null; - const index = fn.parameters.indexOf(decl); + const index = fn.parameters.nodes.indexOf(decl); if (index < 0 || !this.host.paramIsNumber(fnSymbol, index)) return null; nameNode = decl.name; if (decl.initializer) slot.sources.push(decl.initializer); for (const call of this.callSites.get(fnSymbol) ?? []) { - const arg = call.arguments[index]; + const arg = call.arguments.nodes[index]; if (arg) slot.sources.push(arg); else if (!decl.initializer) slot.sources.push(null); } @@ -1020,11 +1020,11 @@ export class NanCodedReads { return symbol ? this.returnSlot(symbol) : null; } if (ts.isCallExpression(p) && p.expression !== e) { - const index = p.arguments.indexOf(e); + const index = p.arguments.nodes.indexOf(e); const fn = this.calleeSymbol(p); if (index < 0 || !fn) return null; const decl = this.eligibleFunction(fn); - const param = decl?.parameters[index]; + const param = decl?.parameters?.nodes[index]; if (!param || !ts.isIdentifier(param.name)) return null; const symbol = this.symbolOf(param.name); return symbol ? this.bindingSlot(symbol) : null; @@ -1493,7 +1493,7 @@ export class NanCodedReads { ) return true; if (ts.isSwitchStatement(p)) { - return p.caseBlock.clauses.every( + return p.caseBlock.clauses.nodes.every( (clause) => !ts.isCaseClause(clause) || this.plainNumber(clause.expression, 0) !== null, ); } diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 5bd563d9d..500267263 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -198,8 +198,8 @@ export class NarrowingStability { this.lowerer.isStdlibGlobal(skipOuter(callee.expression), "console") ) return true; - const index = call.arguments?.indexOf(argument) ?? -1; - const params = declaration.parameters ?? []; + const index = call.arguments?.nodes.indexOf(argument) ?? -1; + const params = declaration.parameters?.nodes ?? []; if (index < 0 || params.length === 0) return false; const param = params[Math.min(index, params.length - 1)]!; if (index >= params.length && param.dotDotDotToken === undefined) return false; @@ -665,21 +665,21 @@ export class NarrowingStability { ); } if (ts.isClassLikeDeclaration(node)) { - if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true; - for (const clause of node.heritageClauses ?? []) { - for (const heritage of clause.types) { + if (node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator)) return true; + for (const clause of node.heritageClauses?.nodes ?? []) { + for (const heritage of clause.types.nodes) { const e = heritage.expression; if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return true; } } - return node.members.some( + return node.members.nodes.some( (member) => ts.isClassStaticBlockDeclaration(member) || (member.name !== undefined && ts.isComputedPropertyName(member.name)) || - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.Decorator) === true || + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.Decorator) === true || (ts.isPropertyDeclaration(member) && member.initializer !== undefined && - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true), + member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true), ); } return true; @@ -688,12 +688,12 @@ export class NarrowingStability { private isInertLibraryCall(call: ts.CallExpression): boolean { if (call.expression.kind === ts.SyntaxKind.SuperKeyword) return false; if (call.expression.kind === ts.SyntaxKind.ImportKeyword) return false; - if (!this.inertArguments(call.arguments)) return false; + if (!this.inertArguments(call.arguments.nodes)) return false; const callee = skipOuter(call.expression); if (ts.isIdentifier(callee)) { if (!INERT_GLOBAL_FUNCTIONS.has(callee.text)) return false; if (!this.lowerer.isStdlibGlobal(callee, callee.text)) return false; - return call.arguments.every((arg) => this.isPrimitive(arg)); + return call.arguments.nodes.every((arg) => this.isPrimitive(arg)); } if (!ts.isPropertyAccessExpression(callee)) return false; const method = callee.name.text; @@ -720,7 +720,7 @@ export class NarrowingStability { const callee = skipOuter(node.expression); if (!ts.isIdentifier(callee) || !INERT_CONSTRUCTORS.has(callee.text)) return false; if (!this.lowerer.isStdlibGlobal(callee, callee.text)) return false; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; if (!this.inertArguments(args)) return false; return args.every((arg) => this.isPrimitive(arg) || this.isBuiltinIterable(arg)); } @@ -959,7 +959,7 @@ function widePosition(node: ts.Expression): WidePosition | null { if (ts.isForStatement(parent) && parent.condition === child) return "test"; if ( (ts.isCallExpression(parent) || ts.isNewExpression(parent)) && - parent.arguments?.some((arg) => arg === child) + parent.arguments?.nodes.some((arg) => arg === child) ) return "context"; // Only slots whose type IS the contextual type: an array or object @@ -997,7 +997,7 @@ function enclosingArgument( parent = parent.parent; } if (!(ts.isCallExpression(parent) || ts.isNewExpression(parent))) return null; - const argument = parent.arguments?.find((arg) => arg === child); + const argument = parent.arguments?.nodes.find((arg) => arg === child); return argument ? { call: parent, argument } : null; } @@ -1016,7 +1016,7 @@ function typedLiteral( if (!slot) return false; if (ts.isVariableDeclaration(slot)) return slot.initializer === literal && !!slot.type; if (ts.isCallExpression(slot) || ts.isNewExpression(slot)) - return slot.arguments?.some((arg) => arg === literal) ?? false; + return slot.arguments?.nodes.some((arg) => arg === literal) ?? false; if (ts.isBinaryExpression(slot)) return slot.operatorToken.kind === ts.SyntaxKind.EqualsToken && slot.right === literal; if (ts.isReturnStatement(slot)) { @@ -1180,13 +1180,13 @@ function forEachWriteTarget(target: ts.Node, visit: (target: ts.Expression) => v return; } if (ts.isArrayLiteralExpression(e)) { - for (const element of e.elements) { + for (const element of e.elements.nodes) { forEachWriteTarget(ts.isSpreadElement(element) ? element.expression : element, visit); } return; } if (ts.isObjectLiteralExpression(e)) { - for (const property of e.properties) { + for (const property of e.properties.nodes) { if (ts.isPropertyAssignment(property)) forEachWriteTarget(property.initializer, visit); else if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) visit(property.name); @@ -1227,7 +1227,7 @@ function reflectiveStore(call: ts.CallExpression): boolean { method === "deleteProperty" || method === "setPrototypeOf")); if (!reflective) return false; - const target = call.arguments[0]; + const target = call.arguments.nodes[0]; return target === undefined || !ts.isObjectLiteralExpression(skipOuter(target)); } diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 1c0cff2c5..3ccd54b64 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -176,7 +176,7 @@ function ffiDeclarationDiagnostic( loc, ); } - if (functionDecls.some((decl) => (decl.typeParameters?.length ?? 0) > 0)) { + if (functionDecls.some((decl) => (decl.typeParameters?.nodes.length ?? 0) > 0)) { return signatureDiag( binding.name, "generic ambient declarations cannot describe one fixed C ABI", @@ -474,18 +474,18 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (expr.questionDotToken !== undefined || expr.typeArguments !== undefined) { signatureError("native bindings support direct, non-generic calls only"); } - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { signatureError("spread arguments do not have a fixed native ABI"); } const expectedParams = ffiSourceParamTypes(binding.params); - if (expr.arguments.length !== expectedParams.length) { + if (expr.arguments.nodes.length !== expectedParams.length) { signatureError( - `this call passes ${expr.arguments.length} argument(s), but the native binding requires exactly ${expectedParams.length}`, + `this call passes ${expr.arguments.nodes.length} argument(s), but the native binding requires exactly ${expectedParams.length}`, ); } const expectedReturn = ffiClassType(binding.returns); const sourceParams = ffiSourceParams(binding); - const args = expr.arguments.map((arg, i) => { + const args = expr.arguments.nodes.map((arg, i) => { const sourceParam = sourceParams[i]!; const expected = expectedParams[i]!; const lowered = lowerer.lowerExprExpecting(arg, expected); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index 0e3eaefd7..ac83dccd8 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -45,7 +45,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { !bindingNeverReassigned(lowerer, symbol, owner) || !isJsSourceFile(declaration.getSourceFile()) || declaration.asteriskToken || - declaration.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) + declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) return false; const objectReturn = (expression: ts.Expression): boolean => @@ -54,7 +54,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { ts.isArrowFunction(expression) || ts.isFunctionExpression(expression) || ts.isNewExpression(expression); - const last = declaration.body.statements.at(-1); + const last = declaration.body.statements.nodes.at(-1); if (!last || !ts.isReturnStatement(last) || !last.expression || !objectReturn(last.expression)) return false; let safe = true; @@ -76,7 +76,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { safe = false; ts.forEachChild(node, (child) => visit(child, nestedArrow)); }; - for (const parameter of declaration.parameters) visit(parameter); + for (const parameter of declaration.parameters.nodes) visit(parameter); visit(declaration.body); return safe; } @@ -104,7 +104,7 @@ export function lowerObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): return { kind: "call", callee: signature.name, - args: lowerer.completeArgs(expr.arguments ?? [], signature.params, loc, expr), + args: lowerer.completeArgs(expr.arguments?.nodes ?? [], signature.params, loc, expr), type: signature.returnType, loc, }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index 5375f3792..2f84e988a 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -52,7 +52,7 @@ export interface EarlyReference { function boundNames(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (ts.isOmittedExpression(el) || el.name === undefined) continue; out.push(...boundNames(el.name)); } @@ -105,10 +105,10 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { for (const sf of lowerer.moduleOrder) { if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue; const declared: { decl: ts.VariableDeclaration; stmt: ts.VariableStatement }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) continue; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { if (decl.initializer === undefined || requireSpecOf(decl.initializer) === null) continue; if (requireEarlyReference(lowerer.program, decl) === null) continue; declared.push({ decl, stmt }); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts index d087a2d61..d3acaff6b 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts @@ -67,7 +67,7 @@ export function indexReadInBounds( ) { // A value-preserving wrapper. } else if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) { - const args = parent.arguments ?? []; + const args = parent.arguments?.nodes ?? []; const at = args.indexOf(child as ts.Expression); if (at < 0 || (ts.isCallExpression(parent) && parent.questionDotToken)) return false; before.push(parent.expression, ...args.slice(0, at)); @@ -80,7 +80,7 @@ export function indexReadInBounds( ts.isVariableDeclaration(parent) && parent.initializer === child && ts.isVariableDeclarationList(parent.parent) && - parent.parent.declarations[0] === parent && + parent.parent.declarations.nodes[0] === parent && ts.isVariableStatement(parent.parent.parent) ) { return boundedBefore(parent.parent.parent, { array, index, facts }, before); @@ -108,7 +108,7 @@ function boundedBefore(statement: ts.Statement, operands: Operands, start: ts.No let parent: ts.Node | undefined = statement.parent; while (parent && !isFunctionBoundary(parent)) { if (isStatementList(parent)) { - const statements = (parent as { statements: ts.NodeArray }).statements; + const statements = (parent as { statements: ts.NodeArray }).statements.nodes; const at = statements.indexOf(child as ts.Statement); for (let i = at - 1; i >= 0; i--) { const statement = statements[i]!; @@ -290,7 +290,7 @@ function assertsSlot(node: ts.Node, operands: Operands): boolean { n.forEachChild(visit); }; if (ts.isVariableStatement(node)) { - for (const declaration of node.declarationList.declarations) + for (const declaration of node.declarationList.declarations.nodes) if (declaration.initializer) visit(declaration.initializer); } else if (ts.isExpressionStatement(node)) visit(node.expression); else if (!ts.isStatement(node)) visit(node); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index e6781be73..2fa8785c9 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -127,7 +127,7 @@ export class ArrayElementStates { /** `[a, ...rest] = source` copies the remaining indices into a new * array: holes become undefined. */ private scanRestPattern(pattern: ts.ArrayBindingPattern): boolean { - const last = pattern.elements[pattern.elements.length - 1]; + const last = pattern.elements.nodes[pattern.elements.nodes.length - 1]; if (!last || ts.isOmittedExpression(last) || !last.dotDotDotToken) return false; // A parameter or nested pattern has no source expression to judge. const owner = pattern.parent; @@ -145,7 +145,7 @@ export class ArrayElementStates { private scanRestAssignment(node: ts.BinaryExpression): boolean { const pattern = peel(node.left); if (!ts.isArrayLiteralExpression(pattern)) return false; - const last = pattern.elements[pattern.elements.length - 1]; + const last = pattern.elements.nodes[pattern.elements.nodes.length - 1]; if (!last || !ts.isSpreadElement(last)) return false; if (!this.mayYieldUndefined(node.right)) return false; return this.addUndefinedTo(last.expression); @@ -159,7 +159,7 @@ export class ArrayElementStates { if (ts.isArrayLiteralExpression(e)) { let undefinedValue = false; let holes = false; - for (const element of e.elements) { + for (const element of e.elements.nodes) { if (ts.isOmittedExpression(element)) holes = true; else if (this.valueMayBeUndefined(element)) undefinedValue = true; } @@ -187,7 +187,7 @@ export class ArrayElementStates { /** Contents of an array a call creates; undefined when the call is not a * known array constructor or method. */ private builtContents(node: ts.CallExpression | ts.NewExpression): Contents | null | undefined { - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; const callee = peel(node.expression); if (ts.isIdentifier(callee)) { if (!this.host.isGlobal(callee, "Array")) return undefined; @@ -352,7 +352,7 @@ export class ArrayElementStates { if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && !buildsArray(node)) return false; if (consumedWhereBuilt(node)) return false; // An empty literal holds nothing. - if (ts.isArrayLiteralExpression(node) && node.elements.length === 0) return false; + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 0) return false; const contents = this.contents(node); if (contents === null || (!contents.undefined && !contents.holes)) return false; const keys = this.keysAsUsed(node); @@ -490,7 +490,7 @@ export class ArrayElementStates { private scanCall(node: ts.CallExpression | ts.NewExpression): boolean { let changed = false; - const args = node.arguments ?? []; + const args = node.arguments?.nodes ?? []; // Spreading a sparse array into a rest parameter fills the holes. if (args.some((arg) => ts.isSpreadElement(arg))) { for (const arg of args) { @@ -593,7 +593,7 @@ export class ArrayElementStates { ts.isCallExpression(source) && ts.isPropertyAccessExpression(source.expression) && (source.expression.name.text === "values" || source.expression.name.text === "entries") && - source.arguments.length === 0 + source.arguments.nodes.length === 0 ) { const receiver = this.host.arrayKeys(source.expression.expression); if (receiver === null || receiver.length > 0) source = source.expression.expression; @@ -614,7 +614,7 @@ export class ArrayElementStates { name.forEachChild(mark); }; if (ts.isVariableDeclarationList(initializer)) { - for (const declaration of initializer.declarations) mark(declaration.name); + for (const declaration of initializer.declarations.nodes) mark(declaration.name); } else mark(initializer); return changed; } @@ -690,12 +690,12 @@ function consumedWhereBuilt(node: ts.Expression): boolean { ) { const arrayAt = ARRAY_ARGUMENT_AT.get(parent.name.text); if (arrayAt === undefined) return true; - const callback = parent.parent.arguments[0]; + const callback = parent.parent.arguments.nodes[0]; return ( callback !== undefined && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && - callback.parameters.length <= arrayAt && - !callback.parameters.some((parameter) => parameter.dotDotDotToken) && + callback.parameters.nodes.length <= arrayAt && + !callback.parameters.nodes.some((parameter) => parameter.dotDotDotToken) && ts.isArrowFunction(callback) ); } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts index 82d944fcd..a19febe3b 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.test.ts @@ -20,7 +20,7 @@ function proofs(body: string): boolean[] { ts.isIdentifier(node.expression) && node.expression.text === "use" ) { - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if (arg && ts.isIdentifier(arg)) results.push( presentAtUse(arg, { diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts index cdcc7bf5c..768932ad3 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts @@ -54,7 +54,7 @@ export function reassignedBindings( } else if (js && ts.isIdentifier(node) && node.text === "arguments") { for (let owner = node.parent; owner; owner = owner.parent) { if (!ts.isFunctionLike(owner) || ts.isArrowFunction(owner)) continue; - for (const parameter of owner.parameters) + for (const parameter of owner.parameters.nodes) for (const bound of boundNames(parameter.name)) mark(bound); break; } @@ -68,7 +68,7 @@ export function reassignedBindings( function boundNames(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const names: ts.Identifier[] = []; - for (const element of name.elements) + for (const element of name.elements.nodes) if (!ts.isOmittedExpression(element) && element.name) names.push(...boundNames(element.name)); return names; } @@ -137,7 +137,7 @@ export function presentAtUse(use: ts.Identifier, bindings: PresenceBindings): bo if (condition && implies(condition, true, bindings)) return true; } else if (ts.isCallExpression(parent) || ts.isNewExpression(parent)) { // Arguments evaluate after the callee and earlier arguments. - const args = parent.arguments ?? []; + const args = parent.arguments?.nodes ?? []; const index = args.indexOf(child as ts.Expression); if (index >= 0 && !isOptionalChainMember(parent)) { if (dereferences(parent.expression, bindings)) return true; @@ -145,7 +145,7 @@ export function presentAtUse(use: ts.Identifier, bindings: PresenceBindings): bo } } if (isStatementList(parent) && !ts.isFunctionDeclaration(child)) { - const statements = (parent as { statements: ts.NodeArray }).statements; + const statements = (parent as { statements: ts.NodeArray }).statements.nodes; const index = statements.indexOf(child as ts.Statement); for (let i = index - 1; i >= 0; i--) { if (statementProvesPresence(statements[i]!, bindings)) return true; @@ -278,7 +278,7 @@ function statementProvesPresence(statement: ts.Statement, bindings: PresenceBind } if (ts.isExpressionStatement(statement)) return dereferences(statement.expression, bindings); if (ts.isVariableStatement(statement)) { - for (const declaration of statement.declarationList.declarations) { + for (const declaration of statement.declarationList.declarations.nodes) { if (declaration.initializer && dereferences(declaration.initializer, bindings)) return true; } return false; @@ -300,7 +300,7 @@ function exitsAbruptly(statement: ts.Statement): boolean { if (ts.isBlock(statement)) { // A labeled statement inside could be the target of an inner break, // which resumes after it; only plain trailing exits count. - const last = statement.statements[statement.statements.length - 1]; + const last = statement.statements.nodes[statement.statements.nodes.length - 1]; return last !== undefined && exitsAbruptly(last); } if (ts.isIfStatement(statement)) @@ -342,11 +342,11 @@ export function dereferences(expression: ts.Expression, bindings: PresenceBindin if (!e.questionDotToken && isBinding(e.expression, bindings)) return true; if (dereferences(e.expression, bindings)) return true; if (isOptionalChainMember(e)) return false; - return e.arguments.some((arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings)); + return e.arguments.nodes.some((arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings)); } if (ts.isNewExpression(e)) { if (dereferences(e.expression, bindings)) return true; - return (e.arguments ?? []).some( + return (e.arguments?.nodes ?? []).some( (arg) => !ts.isSpreadElement(arg) && dereferences(arg, bindings), ); } @@ -381,6 +381,6 @@ export function dereferences(expression: ts.Expression, bindings: PresenceBindin } if (ts.isConditionalExpression(e)) return dereferences(e.condition, bindings); if (ts.isTemplateExpression(e)) - return e.templateSpans.some((span) => dereferences(span.expression, bindings)); + return e.templateSpans.nodes.some((span) => dereferences(span.expression, bindings)); return false; } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts index 2f5badb20..adecfa5a9 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts @@ -226,13 +226,13 @@ export class ArrayOwnership { const arrayAt = ARRAY_ARGUMENT_AT.get(method); if (arrayAt !== undefined) { // An inline callback that cannot see its array argument. - const callback = call.arguments[0]; + const callback = call.arguments.nodes[0]; return ( callback !== undefined && (ts.isArrowFunction(callback) || (ts.isFunctionExpression(callback) && !usesArguments(callback))) && - callback.parameters.length <= arrayAt && - !callback.parameters.some((parameter) => parameter.dotDotDotToken) + callback.parameters.nodes.length <= arrayAt && + !callback.parameters.nodes.some((parameter) => parameter.dotDotDotToken) ); } return this.host.isArray(user.expression); @@ -243,14 +243,14 @@ export class ArrayOwnership { user.operatorToken.kind === ts.SyntaxKind.EqualsToken ) return this.isFreshArray(user.right); - if (ts.isCallExpression(user) && user.arguments[0] === use) { + if (ts.isCallExpression(user) && user.arguments.nodes[0] === use) { const callee = peel(user.expression); return ( ts.isPropertyAccessExpression(callee) && ((callee.name.text === "hasOwn" && this.host.isGlobal(callee.expression, "Object")) || (callee.name.text === "isArray" && this.host.isGlobal(callee.expression, "Array")) || (callee.name.text === "from" && - user.arguments.length === 1 && + user.arguments.nodes.length === 1 && this.host.isGlobal(callee.expression, "Array"))) ); } @@ -348,10 +348,10 @@ export class ArrayOwnership { this.host.isGlobal(callee.expression, "Reflect")); const clone = ts.isIdentifier(callee) && this.host.isGlobal(callee, "structuredClone"); if (walker || clone) - for (const arg of node.arguments) + for (const arg of node.arguments.nodes) this.reflected.push(ts.isSpreadElement(arg) ? arg.expression : arg); } else if (ts.isObjectBindingPattern(node)) { - for (const element of node.elements) { + for (const element of node.elements.nodes) { const name = element.propertyName ?? element.name; // A rest element copies every remaining field. const key = diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts index 334f94d4f..bad2523c8 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts @@ -135,8 +135,8 @@ export class RecordFlowClasses { parameters(): { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] { const out: { symbol: ts.Symbol; fn: ts.Symbol; index: number }[] = []; for (const [fn, decl] of this.functions) { - for (let index = 0; index < decl.parameters.length; index++) { - const symbol = this.host.symbolOf(decl.parameters[index]!.name); + for (let index = 0; index < decl.parameters.nodes.length; index++) { + const symbol = this.host.symbolOf(decl.parameters.nodes[index]!.name); if (symbol && this.bindingSlots.has(symbol)) out.push({ symbol, fn, index }); } } @@ -287,7 +287,7 @@ export class RecordFlowClasses { const key = `${i}:${name}`; if (visitedLiterals.has(key)) continue; visitedLiterals.add(key); - for (const property of this.literalSources[i]!.literal.properties) { + for (const property of this.literalSources[i]!.literal.properties.nodes) { if (ts.isSpreadAssignment(property)) { this.markOpen(field); continue; @@ -364,11 +364,11 @@ export class RecordFlowClasses { if (file.isDeclarationFile || isJsFile(file)) continue; ts.walkPreorder(file, (node) => { if (!ts.isFunctionDeclaration(node) || !node.name || !node.body) return undefined; - if (node.asteriskToken || node.typeParameters?.length) return undefined; + if (node.asteriskToken || node.typeParameters?.nodes.length) return undefined; const flags = ts.getCombinedModifierFlags(node); if (flags & (ts.ModifierFlags.Export | ts.ModifierFlags.Async | ts.ModifierFlags.Ambient)) return undefined; - if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) + if (node.parameters.nodes.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) return undefined; if (usesArguments(node.body)) return undefined; const symbol = this.host.symbolOf(node.name); @@ -380,7 +380,7 @@ export class RecordFlowClasses { !!call && ts.isCallExpression(call) && call.expression === reference && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ); }); if (calledByName) { @@ -404,10 +404,10 @@ export class RecordFlowClasses { for (;;) { if (seen.has(current)) return true; seen.add(current); - const heritage = current.heritageClauses?.find( + const heritage = current.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, ); - const base = heritage?.types[0]?.expression; + const base = heritage?.types?.nodes[0]?.expression; if (!base) return false; const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null; const target = symbol ? this.host.declarationOf(symbol) : undefined; @@ -436,13 +436,13 @@ export class RecordFlowClasses { // classes (`_read`, `handleEvent`) with no call site to see. if (PROTOCOL_METHODS.has(node.name.text) || this.extendsForeignClass(owner)) return undefined; - if (node.asteriskToken || node.typeParameters?.length) return undefined; + if (node.asteriskToken || node.typeParameters?.nodes.length) return undefined; if ( ts.getCombinedModifierFlags(node) & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract) ) return undefined; - if (node.parameters.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) + if (node.parameters.nodes.some((p) => p.dotDotDotToken || !ts.isIdentifier(p.name))) return undefined; if (usesArguments(node.body)) return undefined; const symbol = this.host.symbolOf(node.name); @@ -472,7 +472,7 @@ export class RecordFlowClasses { !!call && ts.isCallExpression(call) && call.expression === parent && - !call.arguments.some(ts.isSpreadElement) + !call.arguments.nodes.some(ts.isSpreadElement) ); } // Any other member, property or key of this name could reach the @@ -505,7 +505,7 @@ export class RecordFlowClasses { if (symbol) this.bindingSlots.set(symbol, this.newSlot(name)); } for (const [symbol, decl] of this.functions) { - for (const parameter of decl.parameters) { + for (const parameter of decl.parameters.nodes) { if (!this.host.mayBeRecord(parameter.name)) continue; const bound = this.host.symbolOf(parameter.name); if (bound) this.bindingSlots.set(bound, this.newSlot(parameter.name)); @@ -518,10 +518,12 @@ export class RecordFlowClasses { if (family[0] !== symbol || family.length < 2) continue; const decls = family.map((member) => this.functions.get(member)!); const first = decls[0]!; - for (let i = 0; i < first.parameters.length; i++) { - const head = this.slotOf(first.parameters[i]!.name); + for (let i = 0; i < first.parameters.nodes.length; i++) { + const head = this.slotOf(first.parameters.nodes[i]!.name); for (const decl of decls.slice(1)) { - const other = decl.parameters[i] ? this.slotOf(decl.parameters[i]!.name) : undefined; + const other = decl.parameters.nodes[i] + ? this.slotOf(decl.parameters.nodes[i]!.name) + : undefined; if (head !== undefined && other !== undefined) this.union(head, other); else if (head !== undefined) this.markOpen(head); else if (other !== undefined) this.markOpen(other); @@ -579,10 +581,10 @@ export class RecordFlowClasses { if (declaration.questionToken) this.markOpen(slot); if (declaration.initializer) this.source(declaration.initializer, slot); const fn = declaration.parent as Callable; - const index = fn.parameters.indexOf(declaration); + const index = fn.parameters.nodes.indexOf(declaration); const fnSymbol = fn.name ? this.host.symbolOf(fn.name) : null; for (const call of fnSymbol ? this.callsOf(fnSymbol) : []) { - const arg = call.arguments[index]; + const arg = call.arguments.nodes[index]; if (arg) this.source(arg, slot); else if (!declaration.initializer) this.markOpen(slot); } @@ -763,14 +765,16 @@ export class RecordFlowClasses { const symbol = this.host.symbolOf(callee); const fn = symbol ? this.functions.get(symbol) : undefined; if (!fn) return false; - const parameter = fn.parameters[user.arguments.indexOf(value as ts.Expression)]; + const parameter = + fn.parameters.nodes[user.arguments.nodes.indexOf(value as ts.Expression)]; return !!parameter && this.isMember(parameter.name); } if (ts.isPropertyAccessExpression(callee)) { const symbol = this.host.symbolOf(callee.name); const method = symbol ? this.functions.get(symbol) : undefined; if (method) { - const parameter = method.parameters[user.arguments.indexOf(value as ts.Expression)]; + const parameter = + method.parameters.nodes[user.arguments.nodes.indexOf(value as ts.Expression)]; return !!parameter && this.isMember(parameter.name); } } diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index b5d5e9200..11443051c 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -40,8 +40,8 @@ export function isPrivateSplitConsumer( init.expression.name.text !== "split" || !lowerer.isStdlibMember(init.expression) || lowerer.mapTypeOf(lowerer.typeOf(init.expression.expression))?.kind !== "string" || - init.arguments.length === 0 || - lowerer.mapTypeOf(lowerer.typeOf(init.arguments[0]!))?.kind !== "string" + init.arguments.nodes.length === 0 || + lowerer.mapTypeOf(lowerer.typeOf(init.arguments.nodes[0]!))?.kind !== "string" ) return false; diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index 43368556c..acb41514f 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -113,7 +113,7 @@ function exits(node: ts.Statement): boolean { ) return true; if (ts.isBlock(node)) { - const last = node.statements[node.statements.length - 1]; + const last = node.statements.nodes[node.statements.nodes.length - 1]; return last !== undefined && exits(last); } return false; @@ -542,7 +542,7 @@ export class StringIndexBounds { return true; } } else if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) { - const statements = node.statements; + const statements = node.statements.nodes; for (let i = statements.indexOf(child as ts.Statement) - 1; i >= 0; i--) { const statement = statements[i]!; if ( diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 1ab731a09..e43748b39 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -140,7 +140,7 @@ export function sideEffectFreeOptionValue(node: ts.Expression): boolean { return sideEffectFreeOptionValue(e.operand); } if (ts.isObjectLiteralExpression(e)) { - return e.properties.every( + return e.properties.nodes.every( (p) => ts.isShorthandPropertyAssignment(p) || (ts.isPropertyAssignment(p) && @@ -149,7 +149,7 @@ export function sideEffectFreeOptionValue(node: ts.Expression): boolean { ); } if (ts.isArrayLiteralExpression(e)) { - return e.elements.every((el) => !ts.isSpreadElement(el) && sideEffectFreeOptionValue(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && sideEffectFreeOptionValue(el)); } return false; } diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index d3bd70d2c..f4f536af9 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -109,12 +109,12 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) { const target = classSymbolKeyOf(lowerer, init); if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); if ( bindingEarlyUse7( lowerer.program, decl.getSourceFile(), - decl.getSourceFile().statements.indexOf(list.parent), + decl.getSourceFile().statements.nodes.indexOf(list.parent), decl, preceding, ) !== null @@ -131,10 +131,10 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const root = registered ? callee.expression : callee; if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null; const arg = - init.arguments.length === 0 + init.arguments.nodes.length === 0 ? null - : init.arguments.length === 1 - ? init.arguments[0]! + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! : undefined; if (arg === undefined || (registered && arg === null)) return null; const description = arg === null ? "" : constantSymbolDescription(lowerer, arg); @@ -143,7 +143,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS // module-scope declaration above can name one fixed layout slot. const sf = decl.getSourceFile(); if (bindingWritten(lowerer, sym, sf)) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); // Creating a function value does not execute its body. The general // require-publication analysis conservatively enters those bodies, but a // prefix containing only imports and function definitions cannot call @@ -164,19 +164,24 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS ); }; const prefixIsInert = - sf.statements - .slice(0, sf.statements.indexOf(list.parent)) + sf.statements.nodes + .slice(0, sf.statements.nodes.indexOf(list.parent)) .every( (statement) => ts.isImportDeclaration(statement) || ts.isFunctionDeclaration(statement) || (ts.isVariableStatement(statement) && - statement.declarationList.declarations.every(inertInitializer)), + statement.declarationList.declarations.nodes.every(inertInitializer)), ) && preceding.every(inertInitializer); if ( !prefixIsInert && - bindingEarlyUse7(lowerer.program, sf, sf.statements.indexOf(list.parent), decl, preceding) !== - null + bindingEarlyUse7( + lowerer.program, + sf, + sf.statements.nodes.indexOf(list.parent), + decl, + preceding, + ) !== null ) return null; // A registry name denotes one symbol even when several source bindings diff --git a/packages/compiler/src/frontend/lowering/symbol-methods.ts b/packages/compiler/src/frontend/lowering/symbol-methods.ts index 093ad058b..f6a8e68e4 100644 --- a/packages/compiler/src/frontend/lowering/symbol-methods.ts +++ b/packages/compiler/src/frontend/lowering/symbol-methods.ts @@ -26,6 +26,6 @@ export function lowerClassSymbolMethodCall( if (!method) return null; const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const args = lowerer.completeArgs(call.arguments, method.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, method.sig.params, loc, call); return accessorCall(lowerer, type.className, name, receiver, args, method.sig.ret, loc); } diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 9247703f5..6ec9af036 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -83,7 +83,7 @@ function rootOf(info: ClassInfo): ClassInfo { function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family { const known = lowerer.threadSlotFamilies.get(expr); if (known !== undefined) return known; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const arg = args[0]; if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr); @@ -211,8 +211,11 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined) lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`); const arity = member === "set" ? 2 : 1; - if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr); + if ( + expr.arguments.nodes.length !== arity || + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) + lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.nodes.length} arguments`, expr); const init = declaringNew(lowerer, callee.expression); if (init === null) lowerer.unsupported( @@ -229,7 +232,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): lowerer.lowerExpr(callee.expression), slotsType, ); - const keyNode = expr.arguments[0]!; + const keyNode = expr.arguments.nodes[0]!; const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key); const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc }; const args = [name, receiver, key]; @@ -244,7 +247,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): case "delete": return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc }; case "set": { - const valueNode = expr.arguments[1]!; + const valueNode = expr.arguments.nodes[1]!; const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value); // `set` returns the slots; a statement discards them. const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType; diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 41e3b2070..c445f1751 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -52,7 +52,7 @@ export function earlyReadVarSymbols( ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ) return undefined; - for (const declaration of node.declarations) { + for (const declaration of node.declarations.nodes) { for (const name of boundIdentifiersOf(declaration.name)) { const symbol = checker.getSymbolAtLocation(name); if (symbol === undefined || !excludesUndefined(lowerer, name)) continue; @@ -101,9 +101,9 @@ function hoistingScopeOf(node: ts.Node): Scope { } function statementsOf(scope: Scope): readonly ts.Statement[] { - if (ts.isSourceFile(scope)) return scope.statements; + if (ts.isSourceFile(scope)) return scope.statements.nodes; const body = scope.body; - return body !== undefined && ts.isBlock(body) ? body.statements : []; + return body !== undefined && ts.isBlock(body) ? body.statements.nodes : []; } /** Whether the binding's declared type has no undefined value. */ @@ -197,7 +197,7 @@ class ScopeAnalysis { : null; if (list !== null) { const loop = !ts.isVariableStatement(s) && !ts.isForStatement(s); - for (const d of list.declarations) { + for (const d of list.declarations.nodes) { if ( (d.initializer !== undefined || loop) && boundIdentifiersOf(d.name).some((n) => checker.getSymbolAtLocation(n) === symbol) diff --git a/packages/compiler/src/frontend/module-source-candidates.ts b/packages/compiler/src/frontend/module-source-candidates.ts index dd468c421..7250a0954 100644 --- a/packages/compiler/src/frontend/module-source-candidates.ts +++ b/packages/compiler/src/frontend/module-source-candidates.ts @@ -36,7 +36,7 @@ export function moduleSourceCandidates( } if (ts.isCallExpression(node)) { result.calls.push(node); - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if ( (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === "require")) && diff --git a/packages/compiler/src/frontend/module-syntax.ts b/packages/compiler/src/frontend/module-syntax.ts index f57f2c947..ac7f550cf 100644 --- a/packages/compiler/src/frontend/module-syntax.ts +++ b/packages/compiler/src/frontend/module-syntax.ts @@ -94,7 +94,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { ts.isImportDeclaration(n) && n.importClause?.namedBindings !== undefined && ts.isNamedImports(n.importClause.namedBindings) && - n.importClause.namedBindings.elements.some((el) => el.name.text === "__require") + n.importClause.namedBindings.elements.nodes.some((el) => el.name.text === "__require") ) { requireHelperImport = n.moduleSpecifier.text; } @@ -102,12 +102,14 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { ts.isExportDeclaration(n) && n.exportClause !== undefined && ts.isNamedExports(n.exportClause) && - n.exportClause.elements.some((el) => (el.propertyName ?? el.name).text === "__require") + n.exportClause.elements.nodes.some( + (el) => (el.propertyName ?? el.name).text === "__require", + ) ) { requireHelperReexport = n.moduleSpecifier.text; } } else if (ts.isCallExpression(n)) { - const arg = n.arguments[0]; + const arg = n.arguments.nodes[0]; if ( n.expression.kind === ts.SyntaxKind.ImportKeyword && arg !== undefined && @@ -119,7 +121,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { n.expression.name.text === "resolve" && ts.isMetaProperty(n.expression.expression) && n.expression.expression.keywordToken === ts.SyntaxKind.ImportKeyword && - n.arguments.length >= 1 && + n.arguments.nodes.length >= 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -132,7 +134,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { n.expression.argumentExpression.text === "require")) && ts.isIdentifier(n.expression.expression) && n.expression.expression.text === "module" && - n.arguments.length === 1 && + n.arguments.nodes.length === 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -140,7 +142,7 @@ export function moduleSpecifiersOfFile(sf: ts.SourceFile): ModuleSpecifiers { } else if ( ts.isIdentifier(n.expression) && (n.expression.text === "require" || n.expression.text === "__require") && - n.arguments.length === 1 && + n.arguments.nodes.length === 1 && arg !== undefined && ts.isStringLiteralLike(arg) ) { @@ -177,14 +179,14 @@ export function sourceImportsOfFile(sf: ts.SourceFile): { spec: string; typeOnly : n.isTypeOnly; out.push({ spec: n.moduleSpecifier.text, typeOnly }); } else if (ts.isCallExpression(n)) { - const arg = n.arguments[0]; + const arg = n.arguments.nodes[0]; if ( arg !== undefined && ts.isStringLiteralLike(arg) && (n.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(n.expression) && n.expression.text === "require" && - n.arguments.length === 1)) + n.arguments.nodes.length === 1)) ) { out.push({ spec: arg.text, typeOnly: false }); } diff --git a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts index 0bd4b551e..89cd0fda1 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts @@ -46,8 +46,8 @@ for (const [name, source] of helperSources) { function declaration(stmt: ts.Statement): ts.VariableDeclaration | ts.FunctionDeclaration | null { if (ts.isFunctionDeclaration(stmt)) return stmt; - if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) { - return stmt.declarationList.declarations[0]!; + if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.nodes.length === 1) { + return stmt.declarationList.declarations.nodes[0]!; } return null; } @@ -109,7 +109,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) string, { stmt: ts.Statement; decl: ts.VariableDeclaration | ts.FunctionDeclaration } >(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const decl = declaration(stmt); if (!decl?.name || !ts.isIdentifier(decl.name)) continue; if (declarations.has(decl.name.text)) return null; @@ -145,23 +145,27 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) return null; - if (call.arguments.length !== 1) return null; - const factory = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const factory = call.arguments.nodes[0]!; if ( !ts.isFunctionExpression(factory) || factory.name || factory.asteriskToken || - factory.modifiers?.length || - factory.parameters.length !== 2 || - factory.body.statements.length !== 1 + factory.modifiers?.nodes.length || + factory.parameters.nodes.length !== 2 || + factory.body.statements.nodes.length !== 1 ) return null; if ( - factory.parameters.some((p) => !ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken) + factory.parameters.nodes.some( + (p) => !ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken, + ) ) return null; - const [exportsName, moduleName] = factory.parameters.map((p) => (p.name as ts.Identifier).text); - const statement = factory.body.statements[0]!; + const [exportsName, moduleName] = factory.parameters.nodes.map( + (p) => (p.name as ts.Identifier).text, + ); + const statement = factory.body.statements.nodes[0]!; if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression)) return null; const assignment = statement.expression; @@ -213,13 +217,13 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) return null; - const [requireCall, mode] = call.arguments; + const [requireCall, mode] = call.arguments.nodes; if ( - call.arguments.length !== 2 || + call.arguments.nodes.length !== 2 || !requireCall || !ts.isCallExpression(requireCall) || !ts.isIdentifier(requireCall.expression) || - requireCall.arguments.length !== 0 || + requireCall.arguments.nodes.length !== 0 || !mode || !ts.isNumericLiteral(mode) || Number(mode.text) !== 1 diff --git a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts index d4c15da22..4a84dcfeb 100644 --- a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts @@ -53,7 +53,7 @@ export function applyNpmStaticJsDocNamepaths( const replacements = new Map(); const visit = (node: ts.Node): void => { for (const doc of node.jsDoc ?? []) - for (const tag of doc.tags ?? []) { + for (const tag of doc.tags?.nodes ?? []) { const expression = tag.typeExpression; if (expression?.kind !== ts.SyntaxKind.JSDocTypeExpression) continue; const start = expression.getStart(sourceFile); @@ -108,13 +108,13 @@ export function applyNpmStaticNullableClassFields( source: string, ): NpmStaticOverloadRewrite | null { const constructors = new Set(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isClassDeclaration(statement) && statement.name) constructors.add(statement.name.text); if (ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly) { const clause = statement.importClause; if (clause?.name) constructors.add(clause.name.text); if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const binding of clause.namedBindings.elements) { + for (const binding of clause.namedBindings.elements.nodes) { if (!binding.isTypeOnly) constructors.add(binding.name.text); } } @@ -128,9 +128,9 @@ export function applyNpmStaticNullableClassFields( const rejectAssignedName = (node: ts.Node): void => { if (ts.isIdentifier(node)) constructors.delete(node.text); else if (ts.isArrayLiteralExpression(node)) - for (const item of node.elements) rejectAssignedName(item); + for (const item of node.elements.nodes) rejectAssignedName(item); else if (ts.isObjectLiteralExpression(node)) - for (const item of node.properties) { + for (const item of node.properties.nodes) { if (ts.isPropertyAssignment(item)) rejectAssignedName(item.initializer); else if (ts.isShorthandPropertyAssignment(item)) rejectAssignedName(item.name); else if (ts.isSpreadAssignment(item)) rejectAssignedName(item.expression); @@ -157,13 +157,13 @@ export function applyNpmStaticNullableClassFields( }; rejectMutable(sourceFile); const inserts: { offset: number; text: string }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement)) continue; const fields = new Map< string, { member: ts.PropertyDeclaration; className?: string; invalid: boolean } >(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -182,7 +182,7 @@ export function applyNpmStaticNullableClassFields( const bindingNames = (name: ts.BindingName): void => { if (ts.isIdentifier(name)) shadowed.add(name.text); else - for (const element of name.elements) + for (const element of name.elements.nodes) if (ts.isBindingElement(element) && element.name !== undefined) bindingNames(element.name); }; @@ -199,7 +199,7 @@ export function applyNpmStaticNullableClassFields( shadowed.add(node.name.text); ts.forEachChild(node, collectBindings); }; - for (const member of statement.members) collectBindings(member); + for (const member of statement.members.nodes) collectBindings(member); const targetField = (node: ts.Node): string | null => { if (ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) return node.name.text; @@ -271,7 +271,7 @@ export function applyNpmStaticNullableClassFields( if (ts.isDeleteExpression(node)) invalidate(node.expression); ts.forEachChild(node, visit); }; - for (const member of statement.members) { + for (const member of statement.members.nodes) { if (hasModifier(member, ts.SyntaxKind.StaticKeyword)) continue; if ( (ts.isConstructorDeclaration(member) || @@ -308,15 +308,15 @@ export function applyNpmStaticFindReturnWidening( source: string, ): NpmStaticOverloadRewrite | null { const insertions: { offset: number; length: number }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement)) continue; - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); if (constructor?.body === undefined) continue; const arrayFields = new Set(); - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) @@ -331,9 +331,9 @@ export function applyNpmStaticFindReturnWidening( ) arrayFields.add(left.name.text); } - for (const member of statement.members) { - if (!ts.isMethodDeclaration(member) || member.body?.statements.length !== 1) continue; - const returned = member.body.statements[0]; + for (const member of statement.members.nodes) { + if (!ts.isMethodDeclaration(member) || member.body?.statements?.nodes.length !== 1) continue; + const returned = member.body.statements.nodes[0]; if ( !returned || !ts.isReturnStatement(returned) || @@ -355,7 +355,7 @@ export function applyNpmStaticFindReturnWidening( returnType === undefined || !ts.isTypeReferenceNode(returnType) || !ts.isIdentifier(returnType.typeName) || - (returnType.typeArguments?.length ?? 0) !== 0 + (returnType.typeArguments?.nodes.length ?? 0) !== 0 ) continue; insertions.push({ offset: returnType.getEnd(), length: " | undefined".length }); @@ -384,7 +384,7 @@ const SAFE_KEYWORD_TYPES = new Set([ function lastJsDocTag(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined { const docs = node.jsDoc; if (docs === undefined || docs.length === 0) return undefined; - for (const tag of docs[docs.length - 1]!.tags ?? []) { + for (const tag of docs[docs.length - 1]!.tags?.nodes ?? []) { if (tag.kind === kind) return tag; } return undefined; @@ -416,7 +416,7 @@ function jsDocReturnTypeOf(node: ts.Node): ts.TypeNode | undefined { const type = jsDocTypeOf(node); if (type === undefined) return undefined; if (ts.isTypeLiteralNode(type)) { - return type.members.find(ts.isCallSignatureDeclaration)?.type; + return type.members.nodes.find(ts.isCallSignatureDeclaration)?.type; } return ts.isFunctionTypeNode(type) ? type.type : undefined; } @@ -447,23 +447,23 @@ function safeTypeText( return safeTypeText(node.type, sourceFile, className); } if (ts.isUnionTypeNode(node)) { - const arms = node.types.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); return arms.some((arm) => arm === null) ? null : arms.join(" | "); } if ( ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === "Record" && - node.typeArguments?.length === 2 && - node.typeArguments[0]?.kind === ts.SyntaxKind.StringKeyword && - node.typeArguments[1]?.kind === ts.SyntaxKind.StringKeyword + node.typeArguments?.nodes.length === 2 && + node.typeArguments.nodes[0]?.kind === ts.SyntaxKind.StringKeyword && + node.typeArguments.nodes[1]?.kind === ts.SyntaxKind.StringKeyword ) { return "Record"; } return ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === className && - (node.typeArguments?.length ?? 0) === 0 + (node.typeArguments?.nodes.length ?? 0) === 0 ? className : null; } @@ -476,7 +476,7 @@ function overloadSignature( if ( !ts.isIdentifier(method.name) || method.type === undefined || - (method.typeParameters?.length ?? 0) !== 0 || + (method.typeParameters?.nodes.length ?? 0) !== 0 || hasModifier(method, ts.SyntaxKind.StaticKeyword) || hasModifier(method, ts.SyntaxKind.PrivateKeyword) || hasModifier(method, ts.SyntaxKind.ProtectedKeyword) @@ -486,7 +486,7 @@ function overloadSignature( const returnType = safeTypeText(method.type, sourceFile, className); if (returnType === null) return null; const parameters: NpmStaticOverloadParameter[] = []; - for (const parameter of method.parameters) { + for (const parameter of method.parameters.nodes) { if ( !ts.isIdentifier(parameter.name) || parameter.name.text === "this" || @@ -513,11 +513,11 @@ export function parseNpmStaticDeclarationOverloads( sourceFile: ts.SourceFile, ): NpmStaticDeclarationOverloads { const classes = new Map>(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || - (statement.typeParameters?.length ?? 0) !== 0 || + (statement.typeParameters?.nodes.length ?? 0) !== 0 || !hasModifier(statement, ts.SyntaxKind.ExportKeyword) || hasModifier(statement, ts.SyntaxKind.DefaultKeyword) ) { @@ -525,7 +525,7 @@ export function parseNpmStaticDeclarationOverloads( } const className = statement.name.text; const groups = new Map(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue; const group = groups.get(member.name.text) ?? []; group.push(member); @@ -558,23 +558,23 @@ function nullableSelfType( sourceFile: ts.SourceFile, className: string, ): string | null { - if (!ts.isUnionTypeNode(node) || node.types.length !== 2) return null; - const arms = node.types.map((type) => safeTypeText(type, sourceFile, className)); + if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; + const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); return arms.includes(className) && arms.includes("null") ? `${className} | null` : null; } function nullableVoidCallbackType(node: ts.TypeNode): string | null { - if (!ts.isUnionTypeNode(node) || node.types.length !== 2) return null; + if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; let callback = false; let nullable = false; - for (let arm of node.types) { + for (let arm of node.types.nodes) { while (ts.isParenthesizedTypeNode(arm)) arm = arm.type; if (ts.isLiteralTypeNode(arm) && arm.literal.kind === ts.SyntaxKind.NullKeyword) nullable = true; else if ( ts.isFunctionTypeNode(arm) && - arm.parameters.length === 0 && - (arm.typeParameters?.length ?? 0) === 0 && + arm.parameters.nodes.length === 0 && + (arm.typeParameters?.nodes.length ?? 0) === 0 && arm.type?.kind === ts.SyntaxKind.VoidKeyword ) callback = true; @@ -588,11 +588,11 @@ export function parseNpmStaticDeclarationProperties( sourceFile: ts.SourceFile, ): NpmStaticDeclarationProperties { const classes = new Map>(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || - (statement.typeParameters?.length ?? 0) !== 0 || + (statement.typeParameters?.nodes.length ?? 0) !== 0 || !hasModifier(statement, ts.SyntaxKind.ExportKeyword) || hasModifier(statement, ts.SyntaxKind.DefaultKeyword) ) { @@ -600,7 +600,7 @@ export function parseNpmStaticDeclarationProperties( } const className = statement.name.text; const properties = new Map(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -626,7 +626,7 @@ export function parseNpmStaticDeclarationProperties( * caller's package-bounded resolution. Bare type dependencies deliberately * do not inherit the opted package's declaration trust. */ export function npmStaticDeclarationReexports(sourceFile: ts.SourceFile): readonly string[] { - return sourceFile.statements.flatMap((statement) => + return sourceFile.statements.nodes.flatMap((statement) => ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier) && @@ -639,13 +639,13 @@ export function npmStaticDeclarationReexports(sourceFile: ts.SourceFile): readon function requireSpecifier(expression: ts.Expression | undefined): string | null { const argument = expression !== undefined && ts.isCallExpression(expression) - ? expression.arguments[0] + ? expression.arguments.nodes[0] : undefined; return expression !== undefined && ts.isCallExpression(expression) && ts.isIdentifier(expression.expression) && expression.expression.text === "require" && - expression.arguments.length === 1 && + expression.arguments.nodes.length === 1 && argument !== undefined && ts.isStringLiteralLike(argument) ? argument.text @@ -661,14 +661,14 @@ export function npmStaticRuntimeClassTargets( classNames: ReadonlySet, ): ReadonlyMap { const localClasses = new Set( - sourceFile.statements.flatMap((statement) => + sourceFile.statements.nodes.flatMap((statement) => ts.isClassDeclaration(statement) && statement.name !== undefined ? [statement.name.text] : [], ), ); const linked = new Map(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isVariableStatement(statement)) { - for (const declaration of statement.declarationList.declarations) { + for (const declaration of statement.declarationList.declarations.nodes) { const specifier = requireSpecifier(declaration.initializer); if ( specifier === null || @@ -676,7 +676,7 @@ export function npmStaticRuntimeClassTargets( !ts.isObjectBindingPattern(declaration.name) ) continue; - for (const element of declaration.name.elements) { + for (const element of declaration.name.elements.nodes) { if (element.dotDotDotToken !== undefined || !ts.isIdentifier(element.name)) continue; const imported = element.propertyName !== undefined && ts.isIdentifier(element.propertyName) @@ -695,7 +695,7 @@ export function npmStaticRuntimeClassTargets( statement.importClause?.namedBindings !== undefined && ts.isNamedImports(statement.importClause.namedBindings) ) { - for (const element of statement.importClause.namedBindings.elements) { + for (const element of statement.importClause.namedBindings.elements.nodes) { if (element.isTypeOnly) continue; linked.set(element.name.text, { imported: element.propertyName?.text ?? element.name.text, @@ -719,7 +719,7 @@ export function npmStaticRuntimeClassTargets( } else if (localClasses.has(local)) targets.set(exported, { specifier: null, localName: local }); }; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( ts.isClassDeclaration(statement) && statement.name !== undefined && @@ -738,7 +738,7 @@ export function npmStaticRuntimeClassTargets( ? statement.moduleSpecifier.text : undefined; if (specifier !== undefined && !specifier.startsWith(".")) continue; - for (const element of statement.exportClause.elements) { + for (const element of statement.exportClause.elements.nodes) { if (!statement.isTypeOnly && !element.isTypeOnly) { record( element.name.text, @@ -773,7 +773,7 @@ export function npmStaticRuntimeClassTargets( left.name.text === "exports" && ts.isObjectLiteralExpression(right) ) { - for (const property of right.properties) { + for (const property of right.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) record(property.name.text, property.name.text); else if ( @@ -791,7 +791,7 @@ export function npmStaticRuntimeClassTargets( function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { const names = new Set(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( ts.isClassDeclaration(statement) && statement.name !== undefined && @@ -805,7 +805,7 @@ function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause) ) { - for (const element of statement.exportClause.elements) + for (const element of statement.exportClause.elements.nodes) names.add(element.propertyName?.text ?? element.name.text); continue; } @@ -833,7 +833,7 @@ function exportedClassNames(sourceFile: ts.SourceFile): ReadonlySet { left.name.text === "exports" && ts.isObjectLiteralExpression(right) ) { - for (const property of right.properties) { + for (const property of right.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) names.add(property.name.text); else if ( @@ -864,13 +864,13 @@ function implementationComment( signatures: readonly NpmStaticOverloadSignature[], ): string | null { if ( - method.parameters.some( + method.parameters.nodes.some( (parameter) => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken !== undefined, ) ) return null; const maxParams = Math.max(...signatures.map((signature) => signature.parameters.length)); - if (method.parameters.length !== maxParams) return null; + if (method.parameters.nodes.length !== maxParams) return null; const params: string[] = []; for (let index = 0; index < maxParams; index++) { const types = [ @@ -882,7 +882,7 @@ function implementationComment( ), ]; if (types.length === 0) return null; - const parameter = method.parameters[index]; + const parameter = method.parameters.nodes[index]; if (parameter === undefined || !ts.isIdentifier(parameter.name)) return null; const name = parameter.name.text; const optional = signatures.some((signature) => { @@ -900,9 +900,9 @@ function implementationComment( function directReturn(statement: ts.Statement): ts.ReturnStatement | null { if (ts.isReturnStatement(statement)) return statement; return ts.isBlock(statement) && - statement.statements.length === 1 && - ts.isReturnStatement(statement.statements[0]!) - ? statement.statements[0]! + statement.statements.nodes.length === 1 && + ts.isReturnStatement(statement.statements.nodes[0]!) + ? statement.statements.nodes[0]! : null; } @@ -926,16 +926,16 @@ function overloadArrayBackingField( ): { field: string; type: string } | null { if ( method.body === undefined || - method.parameters.length !== 1 || - !ts.isIdentifier(method.parameters[0]!.name) + method.parameters.nodes.length !== 1 || + !ts.isIdentifier(method.parameters.nodes[0]!.name) ) return null; const getters = signatures.filter( (signature) => signature.parameters.length === 0 && signature.returnType.endsWith("[]"), ); if (getters.length !== 1) return null; - const parameter = method.parameters[0]!.name.text; - for (const statement of method.body.statements) { + const parameter = method.parameters.nodes[0]!.name.text; + for (const statement of method.body.statements.nodes) { if (!ts.isIfStatement(statement) || !undefinedParameterTest(statement.expression, parameter)) continue; const returned = directReturn(statement.thenStatement); @@ -960,7 +960,7 @@ export function applyNpmStaticDeclarationOverloads( if (declarations.size === 0) return null; const exported = exportedClassNames(sourceFile); const inserts: { offset: number; text: string }[] = []; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if ( !ts.isClassDeclaration(statement) || statement.name === undefined || @@ -969,12 +969,12 @@ export function applyNpmStaticDeclarationOverloads( continue; const classOverloads = declarations.get(statement.name.text); if (classOverloads === undefined) continue; - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); const projectedFields = new Set(); - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name) || @@ -995,7 +995,7 @@ export function applyNpmStaticDeclarationOverloads( constructor?.body !== undefined && !projectedFields.has(backing.field) ) { - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) @@ -1005,7 +1005,7 @@ export function applyNpmStaticDeclarationOverloads( if ( operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isArrayLiteralExpression(right) || - right.elements.length !== 0 || + right.elements.nodes.length !== 0 || !ts.isPropertyAccessExpression(left) || left.expression.kind !== ts.SyntaxKind.ThisKeyword || left.name.text !== backing.field @@ -1039,7 +1039,7 @@ export function applyNpmStaticDeclarationOverloads( inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); else if ( jsDocs.length === 0 && - member.parameters.length === 0 && + member.parameters.nodes.length === 0 && SCALAR_METHOD_RETURNS.has(signatures[0]!.returnType) ) { inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); @@ -1088,7 +1088,7 @@ export function applyNpmStaticDeclarationProperties( const exported = exportedClassNames(sourceFile); const inserts: { offset: number; text: string }[] = []; const classes = new Map(); - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (ts.isClassDeclaration(statement) && statement.name !== undefined) classes.set(statement.name.text, statement); } @@ -1100,9 +1100,9 @@ export function applyNpmStaticDeclarationProperties( if (collecting.has(name)) return new Map(); collecting.add(name); const statement = classes.get(name); - const base = statement?.heritageClauses?.find( + const base = statement?.heritageClauses?.nodes.find( (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, - )?.types[0]?.expression; + )?.types?.nodes[0]?.expression; const properties = new Map(); if (base && ts.isIdentifier(base) && classes.has(base.text)) { // Self-link types belong to their own class. Only the callback ABI @@ -1122,8 +1122,8 @@ export function applyNpmStaticDeclarationProperties( const callbackInitializer = (node: ts.Expression): boolean => { if ( !ts.isArrowFunction(node) || - node.parameters.length !== 0 || - (node.typeParameters?.length ?? 0) !== 0 || + node.parameters.nodes.length !== 0 || + (node.typeParameters?.nodes.length ?? 0) !== 0 || hasModifier(node, ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(node.body) ) @@ -1139,11 +1139,11 @@ export function applyNpmStaticDeclarationProperties( }; return returnsValue(node.body) !== true; }; - for (const statement of sourceFile.statements) { + for (const statement of sourceFile.statements.nodes) { if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue; const properties = propertiesOf(statement.name.text); if (properties.size === 0) continue; - for (const member of statement.members) { + for (const member of statement.members.nodes) { if ( !ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || @@ -1162,12 +1162,12 @@ export function applyNpmStaticDeclarationProperties( if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; inserts.push({ offset: member.getStart(sourceFile), text: `/** @type {${type}} */ ` }); } - const constructor = statement.members.find( + const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => ts.isConstructorDeclaration(member) && member.body !== undefined, ); if (constructor?.body === undefined) continue; - for (const bodyStatement of constructor.body.statements) { + for (const bodyStatement of constructor.body.statements.nodes) { if ( !ts.isExpressionStatement(bodyStatement) || !ts.isBinaryExpression(bodyStatement.expression) diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts index b71d6ca33..84a58d561 100644 --- a/packages/compiler/src/frontend/npm-static-prune.ts +++ b/packages/compiler/src/frontend/npm-static-prune.ts @@ -189,7 +189,7 @@ export function planNpmStaticReexports( if (pureMemo.get(sf) === false || !filePure(sf)) return false; visited.add(sf); const edges: { spec: string; kind?: "import" | "require" }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( ts.isImportDeclaration(stmt) && stmt.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword && @@ -205,8 +205,8 @@ export function planNpmStaticReexports( edges.push({ spec: stmt.moduleSpecifier.text }); } ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.length !== 1) return; - const arg = node.arguments[0]; + if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return; + const arg = node.arguments.nodes[0]; if (!arg || !ts.isStringLiteralLike(arg)) return; if (node.expression.kind === ts.SyntaxKind.ImportKeyword) edges.push({ spec: arg.text, kind: "import" }); @@ -247,7 +247,7 @@ export function planNpmStaticReexports( while (queue.length > 0) { const sf = queue.shift()!; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt)) { if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; @@ -258,7 +258,7 @@ export function planNpmStaticReexports( (clause.namedBindings !== undefined && (ts.isNamespaceImport(clause.namedBindings) || (ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length === 0))) || + clause.namedBindings.elements.nodes.length === 0))) || // `import { type A }` still loads the module, like `import {}`. typeOnlyNamedImport(stmt) ) { @@ -266,7 +266,7 @@ export function planNpmStaticReexports( } else { if (clause.name !== undefined) request(dep, "default"); if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (!element.isTypeOnly) request(dep, (element.propertyName ?? element.name).text); } } @@ -286,8 +286,8 @@ export function planNpmStaticReexports( } } ts.walkPreorder(sf, (node) => { - if (!ts.isCallExpression(node) || node.arguments.length !== 1) return undefined; - const arg = node.arguments[0]; + if (!ts.isCallExpression(node) || node.arguments.nodes.length !== 1) return undefined; + const arg = node.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) return undefined; if ( node.expression.kind === ts.SyntaxKind.ImportKeyword || @@ -308,7 +308,7 @@ export function planNpmStaticReexports( const pruned = new Set(); for (const sf of demanded.keys()) { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( !ts.isExportDeclaration(stmt) || stmt.moduleSpecifier === undefined || diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 21b379893..7938471f3 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -84,15 +84,15 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } * the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */ function getterReturnOf(fn: ts.Node): ts.Expression | null { let body: ts.ConciseBody | undefined; - if (ts.isArrowFunction(fn) && fn.parameters.length === 0) body = fn.body; - else if (ts.isFunctionExpression(fn) && fn.parameters.length === 0) body = fn.body; - else if (ts.isMethodDeclaration(fn) && fn.parameters.length === 0 && fn.body !== undefined) + if (ts.isArrowFunction(fn) && fn.parameters.nodes.length === 0) body = fn.body; + else if (ts.isFunctionExpression(fn) && fn.parameters.nodes.length === 0) body = fn.body; + else if (ts.isMethodDeclaration(fn) && fn.parameters.nodes.length === 0 && fn.body !== undefined) body = fn.body; else return null; if (body === undefined) return null; if (!ts.isBlock(body)) return body; // expression-bodied arrow - if (body.statements.length !== 1) return null; - const ret = body.statements[0]!; + if (body.statements.nodes.length !== 1) return null; + const ret = body.statements.nodes[0]!; return ts.isReturnStatement(ret) && ret.expression !== undefined ? ret.expression : null; } @@ -103,11 +103,11 @@ function exportCallOf( call: ts.CallExpression, ): { target: string; entries: [string, ts.Expression | null][] } | null { if (calleeNameOf(call) !== "__export") return null; - if (call.arguments.length !== 2) return null; - const [target, table] = call.arguments as unknown as [ts.Expression, ts.Expression]; + if (call.arguments.nodes.length !== 2) return null; + const [target, table] = call.arguments.nodes as unknown as [ts.Expression, ts.Expression]; if (!ts.isIdentifier(target) || !ts.isObjectLiteralExpression(table)) return null; const entries: [string, ts.Expression | null][] = []; - for (const prop of table.properties) { + for (const prop of table.properties.nodes) { if (!ts.isPropertyAssignment(prop)) return null; let name: string; if (ts.isIdentifier(prop.name)) name = prop.name.text; @@ -135,8 +135,8 @@ function definePropertyExportOf( ) { return null; } - if (call.arguments.length !== 3) return null; - const [recv, nameArg, desc] = call.arguments as unknown as [ + if (call.arguments.nodes.length !== 3) return null; + const [recv, nameArg, desc] = call.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -145,7 +145,7 @@ function definePropertyExportOf( if (!ts.isStringLiteral(nameArg) || !ts.isObjectLiteralExpression(desc)) return null; const name = nameArg.text; let value: ts.Expression | null = null; - for (const p of desc.properties) { + for (const p of desc.properties.nodes) { if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) { if (p.name.text === "value") value = p.initializer; else if (p.name.text === "get") value = getterReturnOf(p.initializer); @@ -196,7 +196,7 @@ interface ToEsmPlan { * formatting are free; the SHAPE is the contract. */ function recognizedToEsmDecl(stmt: ts.Statement): boolean { if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) return false; const d = decls[0]!; if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined) @@ -204,9 +204,9 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { let init: ts.Expression = d.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; if (!ts.isArrowFunction(init)) return false; - if (init.parameters.length < 2 || init.parameters.length > 3) return false; - const p0 = init.parameters[0]!; - const p1 = init.parameters[1]!; + if (init.parameters.nodes.length < 2 || init.parameters.nodes.length > 3) return false; + const p0 = init.parameters.nodes[0]!; + const p1 = init.parameters.nodes[1]!; if (!ts.isIdentifier(p0.name) || !ts.isIdentifier(p1.name)) return false; const mod = p0.name.text; const isNodeMode = p1.name.text; @@ -224,10 +224,10 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { expr.expression.text !== "__copyProps" ) return false; - if (expr.arguments.length < 2) return false; - const from = expr.arguments[1]!; + if (expr.arguments.nodes.length < 2) return false; + const from = expr.arguments.nodes[1]!; if (!ts.isIdentifier(from) || from.text !== mod) return false; - let to: ts.Expression = expr.arguments[0]!; + let to: ts.Expression = expr.arguments.nodes[0]!; while (ts.isParenthesizedExpression(to)) to = to.expression; if (!ts.isConditionalExpression(to)) return false; const condText = to.condition.getText(); @@ -240,12 +240,12 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean { stamp.expression.text !== "__defProp" ) return false; - if (stamp.arguments.length !== 3) return false; - const nameArg = stamp.arguments[1]!; + if (stamp.arguments.nodes.length !== 3) return false; + const nameArg = stamp.arguments.nodes[1]!; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false; - const desc = stamp.arguments[2]!; + const desc = stamp.arguments.nodes[2]!; if (!ts.isObjectLiteralExpression(desc)) return false; - return desc.properties.some( + return desc.properties.nodes.some( (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -278,10 +278,10 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH degrade: reason, }); - const declStmts = sf.statements.filter( + const declStmts = sf.statements.nodes.filter( (s) => ts.isVariableStatement(s) && - s.declarationList.declarations.some( + s.declarationList.declarations.nodes.some( (d) => ts.isIdentifier(d.name) && d.name.text === "__toESM", ), ); @@ -299,7 +299,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH const callees = new Set(calls.map((c) => c.expression)); const declNames = new Set(); for (const s of declStmts) { - for (const d of (s as ts.VariableStatement).declarationList.declarations) { + for (const d of (s as ts.VariableStatement).declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name); } } @@ -312,12 +312,12 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH * module (pad the access) rather than a member read of its `default`. */ const bindings = new Map(); for (const call of calls) { - const arg0 = call.arguments[0]; + const arg0 = call.arguments.nodes[0]; const spec = arg0 !== undefined ? bareRequireSpecOf(arg0) : null; - if (spec === null || call.arguments.length > 2) return fail(TO_ESM_ARG_DEGRADE); + if (spec === null || call.arguments.nodes.length > 2) return fail(TO_ESM_ARG_DEGRADE); let isNodeMode = false; - if (call.arguments.length === 2) { - const modeArg = call.arguments[1]!; + if (call.arguments.nodes.length === 2) { + const modeArg = call.arguments.nodes[1]!; if (ts.isNumericLiteral(modeArg)) isNodeMode = Number(modeArg.text) !== 0; else if (modeArg.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true; else if (modeArg.kind === ts.SyntaxKind.FalseKeyword) isNodeMode = false; @@ -511,7 +511,7 @@ export function rewriteBundlerCjsSyntax( if (!starSpecsInOrder.includes(spec)) starSpecsInOrder.push(spec); }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExpressionStatement(stmt)) continue; const e = stmt.expression; // `module.exports = ` @@ -524,10 +524,10 @@ export function rewriteBundlerCjsSyntax( if ( ts.isCallExpression(rhs) && calleeNameOf(rhs) === "__toCommonJS" && - rhs.arguments.length === 1 && - ts.isIdentifier(rhs.arguments[0]!) + rhs.arguments.nodes.length === 1 && + ts.isIdentifier(rhs.arguments.nodes[0]!) ) { - esbuildMainTarget = (rhs.arguments[0] as ts.Identifier).text; + esbuildMainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; tablePos = stmt.getStart(sf); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); sawBundleShape = true; @@ -601,8 +601,8 @@ export function rewriteBundlerCjsSyntax( } // `__exportStar(require('spec'), exports);` — the tsc/tslib spelling — // and `__export(require('spec'))`, the legacy twin. - if ((name === "__exportStar" || name === "__export") && e.arguments.length >= 1) { - const spec = bareRequireSpecOf(e.arguments[0]!); + if ((name === "__exportStar" || name === "__export") && e.arguments.nodes.length >= 1) { + const spec = bareRequireSpecOf(e.arguments.nodes[0]!); if (spec !== null) { addStarSpec(spec); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); @@ -612,8 +612,8 @@ export function rewriteBundlerCjsSyntax( } // `__reExport(TARGET, require('spec')[, module.exports]);` — esbuild's // `export * from` plumbing. - if (name === "__reExport" && e.arguments.length >= 2) { - const spec = bareRequireSpecOf(e.arguments[1]!); + if (name === "__reExport" && e.arguments.nodes.length >= 2) { + const spec = bareRequireSpecOf(e.arguments.nodes[1]!); if (spec !== null) { addStarSpec(spec); neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); @@ -733,7 +733,7 @@ export function rewriteBundlerCjsSyntax( * fence the whole statement). Everything else snapshots via a tail * const. */ const immutableTopLevel = new Set(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) immutableTopLevel.add(stmt.name.text); else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) @@ -742,7 +742,7 @@ export function rewriteBundlerCjsSyntax( ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0 ) { - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name)) immutableTopLevel.add(d.name.text); } } @@ -821,9 +821,9 @@ export function rewriteBundlerCjsSyntax( "__importDefault", ]); const helperDecls = new Map(); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; if (decls.length !== 1) continue; const d = decls[0]!; if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) { diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index efe8fb6b9..a81cbf263 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -55,15 +55,15 @@ export function isNpmStaticSubclassArgument( if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip"; if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined; const index = - node.arguments?.findIndex( + node.arguments?.nodes.findIndex( (argument) => argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end, ) ?? -1; if (index < 0) return undefined; const parameter = checker.getResolvedSignature(node)?.getParameters()[index]; - if (parameter && node.arguments) + if (parameter && node.arguments?.nodes) accepted = subtype( - checker.getTypeAtLocation(node.arguments[index]!), + checker.getTypeAtLocation(node.arguments.nodes[index]!), checker.getTypeOfSymbol(parameter), ); return undefined; diff --git a/packages/compiler/src/frontend/pipeline.test.ts b/packages/compiler/src/frontend/pipeline.test.ts index 902441c96..6140d2948 100644 --- a/packages/compiler/src/frontend/pipeline.test.ts +++ b/packages/compiler/src/frontend/pipeline.test.ts @@ -14,7 +14,7 @@ const loader: ProgramLoader = () => { // Resource-ownership tests inject only the load fields the orchestration // reads. Real program loading and native execution have integration tests. const load = { - entry: { fileName: "/main.ts", text: "", statements: [] }, + entry: { fileName: "/main.ts", text: "", statements: { pos: 0, end: 0, nodes: [] } }, moduleOrder: [], dispose: vi.fn(), } as unknown as ReturnType; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index f7ab33429..024c69d98 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -146,7 +146,7 @@ function detectAutoPackages( const owner = npmStaticPackageOfPath(sf.fileName); const viaTransitive = owner !== null && transitive.has(owner); const edges: { spec: string; loc: SrcLoc }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( (isImportDeclaration(stmt) || isExportDeclaration(stmt)) && stmt.moduleSpecifier && @@ -167,7 +167,7 @@ function detectAutoPackages( // anywhere in the package, nested ones included. if (viaTransitive) { for (const call of moduleSourceCandidates(load.program, sf).calls) { - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if ( arg !== undefined && isStringLiteralLike(arg) && diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 8fec2f486..c1bd183f2 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -344,8 +344,8 @@ test("repeated cycle edges reuse a file's binding index", () => { try { const main = load.entry; const peer = load.program.getSourceFile(join(directory, "peer.ts"))!; - const mainImports = main.statements.filter(ts.isImportDeclaration); - const peerImports = peer.statements.filter(ts.isImportDeclaration); + const mainImports = main.statements.nodes.filter(ts.isImportDeclaration); + const peerImports = peer.statements.nodes.filter(ts.isImportDeclaration); const edges = new Map([ [main, mainImports.map((stmt) => ({ dep: peer, stmt }))], [peer, peerImports.map((stmt) => ({ dep: main, stmt }))], diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 7d443318d..bde168c38 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -589,13 +589,13 @@ function createRequireProgramRoots7(program: ts.Program): string[] { for (const node of moduleSourceCandidates(program, sf).calls) { if ( node.questionDotToken !== undefined || - node.arguments.length !== 1 || - !ts.isStringLiteralLike(node.arguments[0]!) || + node.arguments.nodes.length !== 1 || + !ts.isStringLiteralLike(node.arguments.nodes[0]!) || !ts.isIdentifier(node.expression) ) { continue; } - calls.push({ callee: node.expression, spec: node.arguments[0]!.text }); + calls.push({ callee: node.expression, spec: node.arguments.nodes[0]!.text }); } // Root discovery only needs the callee's binding. A first-miss lookup // otherwise fetches symbols and types for every identifier in the file, @@ -1098,8 +1098,8 @@ function requireSpecOf7(node: ts.Node): string | null { } if (!ts.isCallExpression(value)) return null; if (!ts.isIdentifier(value.expression) || value.expression.text !== "require") return null; - if (value.arguments.length !== 1) return null; - const arg = value.arguments[0]!; + if (value.arguments.nodes.length !== 1) return null; + const arg = value.arguments.nodes[0]!; return ts.isStringLiteral(arg) ? arg.text : null; } @@ -1111,7 +1111,7 @@ function requireSpecOf7(node: ts.Node): string | null { function isRequireStatement7(stmt: ts.Statement): boolean { if (ts.isExpressionStatement(stmt)) return requireSpecOf7(stmt.expression) !== null; if (!ts.isVariableStatement(stmt)) return false; - const decls = stmt.declarationList.declarations; + const decls = stmt.declarationList.declarations.nodes; return ( decls.length > 0 && decls.every((d) => d.initializer !== undefined && requireSpecOf7(d.initializer) !== null) @@ -1129,7 +1129,7 @@ function requiresOf7( } if (!ts.isVariableStatement(stmt)) return []; const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = []; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null; if (spec !== null) out.push({ spec, node: decl, decl }); } @@ -1150,8 +1150,8 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { ) { return false; } - if (expr.arguments.length !== 3) return false; - const [recv, nameArg, desc] = expr.arguments as unknown as [ + if (expr.arguments.nodes.length !== 3) return false; + const [recv, nameArg, desc] = expr.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -1165,8 +1165,8 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { recv.name.text === "exports"); if (!isExports) return false; if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false; - if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 1) return false; - const p = desc.properties[0]!; + if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 1) return false; + const p = desc.properties.nodes[0]!; return ( ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && @@ -1187,7 +1187,7 @@ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false; // A replacement export can have setters or non-writable function // properties. Only the wrapper's original object gets this exception. - if (sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false; + if (sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false; if (left.name.text === "__proto__" || left.name.text === "__esModule") return false; let receiver = left.expression; if (isModuleExportsAccess(receiver)) { @@ -1268,7 +1268,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier; - let receiver = call.arguments[0]!; + let receiver = call.arguments.nodes[0]!; if (ts.isPropertyAccessExpression(receiver)) receiver = receiver.expression; if ( !sourceBinding7(program, object) && @@ -1280,7 +1280,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (pureCjsExport7(program, s)) return true; } if (ts.isVariableStatement(s)) { - return s.declarationList.declarations.every(purePrefixDecl7); + return s.declarationList.declarations.nodes.every(purePrefixDecl7); } return false; } @@ -1310,7 +1310,7 @@ export function bindingEarlyUse7( const bound: ts.Identifier[] = []; if (ts.isIdentifier(decl.name)) bound.push(decl.name); else if (ts.isObjectBindingPattern(decl.name)) { - for (const el of decl.name.elements) + for (const el of decl.name.elements.nodes) if (el.name !== undefined && ts.isIdentifier(el.name)) bound.push(el.name); } const bindings = new Map(); @@ -1319,7 +1319,7 @@ export function bindingEarlyUse7( if (sym) bindings.set(sym, id.text); } if (bindings.size === 0) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; const hoisted = new Map(); for (const s of stmts) { if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name) { @@ -1393,9 +1393,9 @@ function requireEarlyReference7( const stmt = decl.parent?.parent; const sf = decl.getSourceFile(); if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { - const stmts = sf.statements; + const stmts = sf.statements.nodes; const k = stmts.indexOf(stmt); - const declarations = stmt.declarationList.declarations; + const declarations = stmt.declarationList.declarations.nodes; // Earlier declarators run too: `const x = read(), dep = require('./dep')`. const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); let firstRunnable = program.analysis.requireFirstRunnable.get(sf); @@ -1446,7 +1446,7 @@ export function requireBindingEarlyUse7( if (sym) bindings.add(sym); } if (bindings.size === 0) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; const cjs = isCjsJsFile7(sf, program); const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { const parent = id.parent; @@ -1471,7 +1471,7 @@ export function requireBindingEarlyUse7( if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) { out.push(e); } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || @@ -1489,7 +1489,7 @@ export function requireBindingEarlyUse7( if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) { addUnit(s.name, s); } else if (i < k && ts.isVariableStatement(s)) { - for (const d of s.declarationList.declarations) { + for (const d of s.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue; const out: ts.Node[] = []; storedCallables(d.initializer, out); @@ -1521,7 +1521,7 @@ export function requireBindingEarlyUse7( for (const s of stmts.slice(0, k)) { ts.walkPreorder(s, (node) => { if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) { - for (const arg of node.arguments ?? []) callbacks(arg); + for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); @@ -1702,7 +1702,7 @@ export function requireBindingEarlyUse7( return; } if (ts.isObjectLiteralExpression(value)) { - for (const p of value.properties) { + for (const p of value.properties.nodes) { if (ts.isShorthandPropertyAssignment(p)) { const sym = checker.getShorthandAssignmentValueSymbol(p); if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier; @@ -1823,9 +1823,9 @@ export function requireBindingEarlyUse7( ts.isPropertyAccessExpression(e.expression) && isWrapperName(e.expression.expression, "Object") && e.expression.name.text === "defineProperty" && - e.arguments.length === 3 + e.arguments.nodes.length === 3 ) { - const [recv, key, desc] = e.arguments as unknown as [ + const [recv, key, desc] = e.arguments.nodes as unknown as [ ts.Expression, ts.Expression, ts.Expression, @@ -1834,7 +1834,7 @@ export function requireBindingEarlyUse7( if (!ts.isObjectLiteralExpression(desc)) return false; const keyTarget: Target = { kind: "key", key: key.text }; const link = (node: ts.Node): void => linkTo(keyTarget, node); - for (const p of desc.properties) { + for (const p of desc.properties.nodes) { if ( (ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) && ts.isIdentifier(p.name) && @@ -2032,8 +2032,8 @@ function staticCreateRequireUse7(expr: ts.Expression): boolean { ts.isCallExpression(call) && call.expression === use && !call.questionDotToken && - call.arguments.length === 1 && - ts.isStringLiteralLike(call.arguments[0]!) + call.arguments.nodes.length === 1 && + ts.isStringLiteralLike(call.arguments.nodes[0]!) ); } @@ -2072,11 +2072,13 @@ function stableCreateRequireBindingReason7( return "its createRequire binding is reassigned, escapes, or uses a computed specifier"; } } - const preceding = stmt.declarationList.declarations.slice( + const preceding = stmt.declarationList.declarations.nodes.slice( 0, - stmt.declarationList.declarations.indexOf(decl), + stmt.declarationList.declarations.nodes.indexOf(decl), ); - if (bindingEarlyUse7(program, sf, sf.statements.indexOf(stmt), decl, preceding) !== null) { + if ( + bindingEarlyUse7(program, sf, sf.statements.nodes.indexOf(stmt), decl, preceding) !== null + ) { return "its createRequire binding can be used before initialization"; } return null; @@ -2093,11 +2095,11 @@ function isCreateRequireBaseCall7(program: ts.Program, expr: ts.Expression): boo init.questionDotToken !== undefined || !ts.isIdentifier(init.expression) || !isCreateRequireImport7(program, init.expression) || - init.arguments.length !== 1 + init.arguments.nodes.length !== 1 ) { return false; } - const base = stripRequireCasts7(init.arguments[0]!); + const base = stripRequireCasts7(init.arguments.nodes[0]!); if (ts.isIdentifier(base) && base.text === "__filename") return !sourceBinding7(program, base); return ( ts.isPropertyAccessExpression(base) && @@ -2124,7 +2126,7 @@ function npmStaticModuleImportReason7( if (!bindings || !ts.isNamedImports(bindings)) return null; const checker = program.getTypeChecker(); const sf = stmt.getSourceFile(); - for (const specifier of bindings.elements) { + for (const specifier of bindings.elements.nodes) { if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue; const imported = checker.getSymbolAtLocation(specifier.name); if (!imported) return "its createRequire import could not be resolved"; @@ -2217,7 +2219,7 @@ const CJS_WRAPPER_NAMES = new Set(["require", "module", "exports", "__dirname", function bindingDeclaresCjsWrapper7(name: ts.BindingName): ts.Identifier | null { if (ts.isIdentifier(name)) return CJS_WRAPPER_NAMES.has(name.text) ? name : null; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (el.name === undefined) continue; const hit = bindingDeclaresCjsWrapper7(el.name); if (hit !== null) return hit; @@ -2239,7 +2241,7 @@ function insideFunctionLike7(node: ts.Node): boolean { * import/export forms, import.meta, top-level await/for-await, or a top-level * lexical declaration that would redeclare a CommonJS wrapper binding. */ function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt; if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) { return stmt; @@ -2255,7 +2257,7 @@ function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null { ts.isVariableStatement(stmt) && (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { const hit = bindingDeclaresCjsWrapper7(decl.name); if (hit !== null) return hit; } @@ -2394,7 +2396,7 @@ function selfImportTdzFences7( }; if (clause.name !== undefined) checkNamed(clause.name); if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) { - for (const el of clause.namedBindings.elements) { + for (const el of clause.namedBindings.elements.nodes) { if (!el.isTypeOnly) checkNamed(el.name); } } @@ -2572,7 +2574,7 @@ function inTypePosition7(node: ts.Node): boolean { * coercion of a user object) disqualifies the module's cycles. */ function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | null { const nonInert = topLevelInertness7(program); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const offence = nonInert(stmt); if (offence !== null) return offence; } @@ -2618,7 +2620,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).some( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( (m) => m.kind === ts.SyntaxKind.Decorator, ); /** No function-like node anywhere in the subtree — arguments to builtin @@ -2636,11 +2638,11 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod }; const inertClass = (c: ts.ClassLikeDeclaration): ts.Node | null => { if (hasDecorator(c)) return c; - for (const h of c.heritageClauses ?? []) { + for (const h of c.heritageClauses?.nodes ?? []) { if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue; - for (const t of h.types) if (!inert(t.expression)) return t.expression; + for (const t of h.types.nodes) if (!inert(t.expression)) return t.expression; } - for (const m of c.members) { + for (const m of c.members.nodes) { if (ts.isClassStaticBlockDeclaration(m)) return m; if (hasDecorator(m)) return m; const memberName = (m as { name?: ts.PropertyName }).name; @@ -2699,7 +2701,9 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return dtsRooted(e) && inert(e.argumentExpression) && primitiveTyped(e.argumentExpression); } if (ts.isTemplateExpression(e)) { - return e.templateSpans.every((s) => inert(s.expression) && primitiveTyped(s.expression)); + return e.templateSpans.nodes.every( + (s) => inert(s.expression) && primitiveTyped(s.expression), + ); } if (ts.isVoidExpression(e) || ts.isTypeOfExpression(e)) return inert(e.expression); if (ts.isPrefixUnaryExpression(e)) { @@ -2721,9 +2725,9 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod if (ts.isConditionalExpression(e)) return inert(e.condition) && inert(e.whenTrue) && inert(e.whenFalse); if (ts.isArrayLiteralExpression(e)) - return e.elements.every((el) => !ts.isSpreadElement(el) && inert(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inert(el)); if (ts.isObjectLiteralExpression(e)) { - return e.properties.every((p) => { + return e.properties.nodes.every((p) => { if (ts.isShorthandPropertyAssignment(p)) return true; if (!ts.isPropertyAssignment(p) || ts.isComputedPropertyName(p.name)) return false; return inert(p.initializer); @@ -2731,7 +2735,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod } if (ts.isCallExpression(e) || ts.isNewExpression(e)) { if (!dtsRooted(e.expression) || !ts.isIdentifier(chainRoot7(e.expression))) return false; - const args = e.arguments ?? []; + const args = e.arguments?.nodes ?? []; // A CALLABLE argument is admissible when it is itself dts-rooted: // a builtin-owned function value (the `promisify(fs.readFile)` // at every cycle member's top level) is runtime-implemented — even @@ -2763,11 +2767,13 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod return null; } if (ts.isEnumDeclaration(stmt)) { - return stmt.members.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null; + return ( + stmt.members.nodes.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null + ); } if (ts.isClassDeclaration(stmt)) return inertClass(stmt); if (ts.isVariableStatement(stmt)) { - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (!ts.isIdentifier(d.name)) return d.name; // patterns can invoke getters/iterators if (d.initializer !== undefined && !inert(d.initializer)) return d.initializer; } @@ -2820,7 +2826,7 @@ function backEdgeUseOffence7( if (clause.namedBindings !== undefined) { if (ts.isNamespaceImport(clause.namedBindings)) bindingNames.push(clause.namedBindings.name); else - for (const el of clause.namedBindings.elements) + for (const el of clause.namedBindings.elements.nodes) if (!el.isTypeOnly) bindingNames.push(el.name); } for (const bindingName of bindingNames) { @@ -3016,7 +3022,7 @@ function cjsCycleRefusal7( if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true; if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart); if (ts.isObjectLiteralExpression(e)) { - return e.properties.every((p) => + return e.properties.nodes.every((p) => ts.isShorthandPropertyAssignment(p) ? true : ts.isPropertyAssignment(p) @@ -3028,7 +3034,7 @@ function cjsCycleRefusal7( ); } if (ts.isArrayLiteralExpression(e)) { - return e.elements.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); + return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); } if (ts.isPropertyAccessExpression(e)) return builtinRoot(e); if (ts.isVoidExpression(e)) return literal(e.expression); @@ -3036,13 +3042,13 @@ function cjsCycleRefusal7( return ( requireSpecOf7(e) === null && builtinRoot(e.expression) && - (e.arguments ?? []).every(literal) + (e.arguments?.nodes ?? []).every(literal) ); } return false; }; const inertPart = (part: ts.Node): boolean => { - if (ts.isHeritageClause(part)) return part.types.every((t) => inertExpr(t.expression)); + if (ts.isHeritageClause(part)) return part.types.nodes.every((t) => inertExpr(t.expression)); if (ts.isComputedPropertyName(part)) return inertExpr(part.expression); return ts.isExpression(part) && inertExpr(part); }; @@ -3080,17 +3086,17 @@ function cjsCycleRefusal7( call.expression.expression.text === "Object" && !sourceBinding7(program, call.expression.expression) && call.expression.name.text === "defineProperty" && - call.arguments.length === 3 && - exportObject(call.arguments[0]!) && - ts.isStringLiteralLike(call.arguments[1]!) && - inertExpr(call.arguments[2]!) + call.arguments.nodes.length === 3 && + exportObject(call.arguments.nodes[0]!) && + ts.isStringLiteralLike(call.arguments.nodes[1]!) && + inertExpr(call.arguments.nodes[2]!) ); }; for (const m of comp) { if (!isCjsJsFile7(m, program)) { return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`; } - const stmts = m.statements; + const stmts = m.statements.nodes; // The first statement that can run another module's code. let firstEntry = stmts.length; for (let i = 0; i < stmts.length; i++) { @@ -3101,7 +3107,7 @@ function cjsCycleRefusal7( (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) || (ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) || (ts.isVariableStatement(st) && - st.declarationList.declarations.every( + st.declarationList.declarations.nodes.every( (d) => ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)), )) || @@ -3236,7 +3242,7 @@ function exactCycleRefusal7( if (hasTopLevelAwait7(m)) { return `${m.fileName} uses top-level await inside the cycle`; } - for (const stmt of m.statements) { + for (const stmt of m.statements.nodes) { if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`; } @@ -3430,7 +3436,7 @@ function computeCycleEarlyBindings7( const names = new Set(); let dirty = !known; evaluation.forEach((m, rank) => { - for (const stmt of m.statements) { + for (const stmt of m.statements.nodes) { const offence = nonInert(stmt); const exposed = dirty || offence !== null; const declared: { name: ts.Identifier; kind: CycleEarlyBinding["kind"] }[] = []; @@ -3442,7 +3448,7 @@ function computeCycleEarlyBindings7( ) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { for (const name of boundNames7(d.name)) { declared.push({ name, kind: lexical ? "lexical" : "var" }); } @@ -3614,7 +3620,7 @@ function moduleEarlyBindingsOf7( units.push(e); immediate.set(e, classSyncParts7(e)); } else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if ( ts.isMethodDeclaration(p) || ts.isGetAccessorDeclaration(p) || @@ -3630,11 +3636,11 @@ function moduleEarlyBindingsOf7( } else if (ts.isPropertyAssignment(p)) storedIn(p.initializer, units); } } else if (ts.isArrayLiteralExpression(e)) { - for (const el of e.elements) if (!ts.isSpreadElement(el)) storedIn(el, units); + for (const el of e.elements.nodes) if (!ts.isSpreadElement(el)) storedIn(el, units); } else if (ts.isCallExpression(e)) { // `const f = wrap(callback)` where wrap only returns closures over // the callback: it runs when the stored result is called. - e.arguments.forEach((arg, index) => { + e.arguments.nodes.forEach((arg, index) => { let fn: ts.Expression = arg; while (ts.isParenthesizedExpression(fn)) fn = fn.expression; if ( @@ -3645,7 +3651,7 @@ function moduleEarlyBindingsOf7( }); } }; - sf.statements.forEach((stmt, index) => { + sf.statements.nodes.forEach((stmt, index) => { if (ambient(stmt)) return; if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) { store(stmt.name, [stmt]); @@ -3655,7 +3661,7 @@ function moduleEarlyBindingsOf7( candidateNames.add(stmt.name.text); } else if (ts.isVariableStatement(stmt)) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if (ts.isIdentifier(d.name) && d.initializer !== undefined) { const units: ts.Node[] = []; storedIn(d.initializer, units); @@ -3702,7 +3708,7 @@ function moduleEarlyBindingsOf7( while (ts.isParenthesizedExpression(e)) e = e.expression; if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); else if (ts.isObjectLiteralExpression(e)) { - for (const p of e.properties) { + for (const p of e.properties.nodes) { if (ts.isMethodDeclaration(p)) stored.add(p); else if (ts.isPropertyAssignment(p)) callbacks(p.initializer); } @@ -3713,7 +3719,7 @@ function moduleEarlyBindingsOf7( (ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJobCallee7(checker, node.expression) ) { - for (const arg of node.arguments ?? []) callbacks(arg); + for (const arg of node.arguments?.nodes ?? []) callbacks(arg); } return undefined; }); @@ -3793,7 +3799,7 @@ function moduleEarlyBindingsOf7( }); } }; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isClassDeclaration(stmt)) { // The binding initializes once the class definition, static // initializers included, has run. @@ -3803,7 +3809,7 @@ function moduleEarlyBindingsOf7( } else if (ts.isVariableStatement(stmt)) { // Declarators run in order: a later declarator's initializer cannot // observe an earlier binding before it was initialized. - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { visit(d); drain(); report(d); @@ -3848,10 +3854,11 @@ function returnsOnlyClosuresOver7( if (fn === undefined) return false; const like = fn as ts.FunctionLikeDeclaration; const body = like.body; - const param = like.parameters[index]; + const param = like.parameters.nodes[index]; if (body === undefined || !ts.isBlock(body) || param === undefined) return false; if (!ts.isIdentifier(param.name) || param.dotDotDotToken !== undefined) return false; - if (like.parameters.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) return false; + if (like.parameters.nodes.slice(0, index).some((p) => p.dotDotDotToken !== undefined)) + return false; const paramName = param.name.text; let deferred = true; ts.walkPreorder(body, (node) => { @@ -3897,11 +3904,11 @@ function returnsOnlyClosuresOver7( * call or a construction. */ function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] { const decorators = (n: ts.Node): ts.Node[] => - ((n as { modifiers?: readonly ts.Node[] }).modifiers ?? []).filter( + ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).filter( (m) => m.kind === ts.SyntaxKind.Decorator, ); - const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses ?? [])]; - for (const m of c.members) { + const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses?.nodes ?? [])]; + for (const m of c.members.nodes) { parts.push(...decorators(m)); const memberName = (m as { name?: ts.PropertyName }).name; if (memberName !== undefined && ts.isComputedPropertyName(memberName)) parts.push(memberName); @@ -4027,7 +4034,7 @@ function foldablePosition7(ref: ts.Identifier): boolean { function boundNames7(name: ts.BindingName): ts.Identifier[] { if (ts.isIdentifier(name)) return [name]; const out: ts.Identifier[] = []; - for (const el of name.elements) { + for (const el of name.elements.nodes) { if (ts.isOmittedExpression(el) || el.name === undefined) continue; out.push(...boundNames7(el.name)); } @@ -4040,7 +4047,7 @@ function typeOnlyNamespace7(decl: ts.ModuleDeclaration): boolean { let body = decl.body; while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body; if (body === undefined || !ts.isModuleBlock(body)) return true; - return body.statements.every( + return body.statements.nodes.every( (s) => ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || @@ -4274,7 +4281,7 @@ function preflight7(load: LoadResult): { names = new Set(); builtinNamespaceNames.set(d.fileName, names); const sf = p.getSourceFile(d.fileName); - for (const stmt of sf?.statements ?? []) { + for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const bindings = stmt.importClause?.namedBindings; @@ -4485,7 +4492,7 @@ function preflight7(load: LoadResult): { if (ambientModuleDecls === null) { ambientModuleDecls = { names: new Set(), patterns: [] }; for (const f of program.getSourceFiles()) { - for (const s of f.statements) { + for (const s of f.statements.nodes) { if (!ts.isModuleDeclaration(s) || !ts.isStringLiteral(s.name)) continue; const name = s.name.text; if (name.includes("*")) ambientModuleDecls.patterns.push(name); @@ -4536,7 +4543,7 @@ function preflight7(load: LoadResult): { edges.set(sf, deps); refusals.set(sf, []); depPositions.set(sf, []); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportEqualsDeclaration(stmt)) { // The ENTITY form (`import a = A`, `import x = N.y`) is namespace // alias plumbing — the lowerer resolves references through it and @@ -4898,7 +4905,7 @@ function preflight7(load: LoadResult): { } { const jsRequireFile = isJsSourceFileName(sf.fileName); - const stmts = sf.statements; + const stmts = sf.statements.nodes; // A binding that code running before the require can reach. Node // initializes it AT the require: a const/let read earlier throws // ReferenceError, which the lowering reproduces with an @@ -4987,7 +4994,7 @@ function preflight7(load: LoadResult): { pattern: ts.ObjectBindingPattern, module: string | null, ): boolean => - pattern.elements.every((e) => { + pattern.elements.nodes.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; // A nested pattern under a named export of a program module @@ -5363,7 +5370,7 @@ function cjsNamedImportLinkCheck( spec: string; dep: ts.SourceFile; }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( !ts.isImportDeclaration(stmt) || stmt.moduleSpecifier === undefined || @@ -5376,7 +5383,7 @@ function cjsNamedImportLinkCheck( const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; - for (const e of clause.namedBindings.elements) { + for (const e of clause.namedBindings.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; entries.push({ local: e.name.text, exportName: nameNode.text, nameNode, spec, dep }); @@ -5387,14 +5394,14 @@ function cjsNamedImportLinkCheck( if (!visible(e.dep, e.exportName)) return { exportName: e.exportName, spec: e.spec, nameNode: e.nameNode, sf }; } - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue; const spec = stmt.moduleSpecifier.text; const dep = cjsDepOf(sf, spec); if (dep === null) continue; - for (const e of stmt.exportClause.elements) { + for (const e of stmt.exportClause.elements.nodes) { if (e.isTypeOnly) continue; const nameNode = e.propertyName ?? e.name; if (!visible(dep, nameNode.text)) return { exportName: nameNode.text, spec, nameNode, sf }; @@ -5411,7 +5418,7 @@ function cjsNamedImportLinkCheck( const dfs = (sf: ts.SourceFile): BadCjsImport | null => { if (visited.has(sf)) return null; visited.add(sf); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; @@ -5456,7 +5463,7 @@ function cjsNamedImportLinkCheck( // it is CommonJS (module_job.js consults the ROOT job's moduleRequests/ // commonJsDeps — a deep module's failure keeps the generic wording). let flavored = false; - for (const stmt of entry.statements) { + for (const stmt of entry.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if ( @@ -5600,7 +5607,7 @@ function analyzeEsmNamedImportLinks( // symbols make the name unavailable. `default` never star-reexports. if (name === "default") return undefined; let starExport: ts.Symbol | undefined; - for (const stmt of dep.statements) { + for (const stmt of dep.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly || stmt.exportClause !== undefined) continue; if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; @@ -5623,7 +5630,7 @@ function analyzeEsmNamedImportLinks( nameNode: ts.Node; dep: ts.SourceFile; }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const clause = stmt.importClause; if (clause === undefined || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; @@ -5640,7 +5647,7 @@ function analyzeEsmNamedImportLinks( }); } if (clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings)) continue; - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; imports.push({ @@ -5667,7 +5674,7 @@ function analyzeEsmNamedImportLinks( } } - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue; const moduleSpecifier = stmt.moduleSpecifier; const exportClause = stmt.exportClause; @@ -5676,7 +5683,7 @@ function analyzeEsmNamedImportLinks( const spec = moduleSpecifier.text; const dep = resolveEdge(sf, spec); if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue; - for (const element of exportClause.elements) { + for (const element of exportClause.elements.nodes) { if (element.isTypeOnly) continue; const nameNode = element.propertyName ?? element.name; if (runtimeExport(dep, nameNode.text) === undefined) { @@ -5694,7 +5701,7 @@ function analyzeEsmNamedImportLinks( const dfs = (sf: ts.SourceFile): BadEsmImport | null => { if (visited.has(sf)) return null; visited.add(sf); - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) continue; if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; @@ -5792,12 +5799,12 @@ function earlierLinkCrash( if (sf === undefined) return first.loc.start < second.loc.start ? first : second; const rank = (crash: StartupCrash): [number, number] => { const entries: { local: string; start: number; kind: 0 | 1 }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) { const clause = stmt.importClause; if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue; if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) { - for (const element of clause.namedBindings.elements) { + for (const element of clause.namedBindings.elements.nodes) { if (!element.isTypeOnly) { entries.push({ local: element.name.text, @@ -5813,7 +5820,7 @@ function earlierLinkCrash( const direct = entries.findIndex((entry) => entry.start === crash.loc.start); if (direct >= 0) return [0, direct]; let reexport = 0; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const exportClause = ts.isExportDeclaration(stmt) ? stmt.exportClause : undefined; if ( !ts.isExportDeclaration(stmt) || @@ -5822,7 +5829,7 @@ function earlierLinkCrash( !ts.isNamedExports(exportClause) ) continue; - for (const element of exportClause.elements) { + for (const element of exportClause.elements.nodes) { if ( !element.isTypeOnly && (element.propertyName ?? element.name).getStart(sf) === crash.loc.start @@ -5890,7 +5897,7 @@ export function isJsSourceFile(sf: ts.SourceFile): boolean { export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean { if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false; let checked = projectCheckJs; - const head = sf.text.slice(0, sf.statements[0]?.getStart(sf) ?? sf.text.length); + const head = sf.text.slice(0, sf.statements.nodes[0]?.getStart(sf) ?? sf.text.length); for (const line of head.split("\n")) { const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line); if (directive) checked = directive[1] === undefined; @@ -5903,7 +5910,7 @@ export function orderedImportsOf( sf: ts.SourceFile, ): { stmt: ts.Statement; dep: ts.SourceFile | null }[] { const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue; if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue; @@ -6025,7 +6032,7 @@ export function cjsVoidInitPreambleReals( Extract, { kind: "member" }> >(); for (const name of names) { - const real = sf.statements + const real = sf.statements.nodes .map((s) => cjsExportAssignmentOf(s)) .find( (other) => @@ -6085,7 +6092,7 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null { if (!ts.isSourceFile(sf)) return null; const mine = cjsExportAssignmentOf(stmt); if (!mine) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; let lastTable = -1; for (let i = 0; i < stmts.length; i++) { if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; @@ -6123,15 +6130,15 @@ export function cjsExportTargetLiteral( ts.isNewExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "Proxy" && - (e.arguments?.length ?? 0) >= 1 + (e.arguments?.nodes.length ?? 0) >= 1 ) { - return cjsExportTargetLiteral(e.arguments![0]!, sf); + return cjsExportTargetLiteral(e.arguments!.nodes[0]!, sf); } if (ts.isIdentifier(e)) { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) continue; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if ( d.name !== undefined && ts.isIdentifier(d.name) && @@ -6157,7 +6164,7 @@ export function cjsClassExprWholeExportOf( sf: ts.SourceFile, ): { classExpr: ts.ClassExpression; stmt: ts.Statement } | null { let last: { classExpr: ts.ClassExpression; stmt: ts.Statement } | null = null; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs || cjs.kind !== "table") continue; let rhs: ts.Expression = cjs.expr.right; @@ -6190,7 +6197,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; const sf = obj.getSourceFile(); if (!isJsSourceFile(sf)) return false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) { return cjsExportTargetLiteral(cjs.expr.right, sf) === obj; @@ -6294,7 +6301,7 @@ function receiverRoot(text: string, pos: number): string | null { /** Whether `name` is bound in `fileName` by an import of a Node builtin. */ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean { const sf = load.program.getSourceFile(fileName); - for (const stmt of sf?.statements ?? []) { + for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const clause = stmt.importClause; @@ -6304,7 +6311,7 @@ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): b if ( bindings && ts.isNamedImports(bindings) && - bindings.elements.some((e) => e.name.text === name) + bindings.elements.nodes.some((e) => e.name.text === name) ) return true; } diff --git a/packages/compiler/src/frontend/services.test.ts b/packages/compiler/src/frontend/services.test.ts index 03afcd655..bd172bbc5 100644 --- a/packages/compiler/src/frontend/services.test.ts +++ b/packages/compiler/src/frontend/services.test.ts @@ -80,7 +80,7 @@ test("syntax sessions reuse one connection and returned ASTs survive edits and c services.close(); services.close(); expect(connections[0]!.close).toHaveBeenCalledTimes(1); - expect(before.statements[0]!.getText()).toBe(before.text); + expect(before.statements.nodes[0]!.getText()).toBe(before.text); }); test("program hosts remain usable after their creating services close", () => { diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index ded7c5225..248b3f922 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -360,9 +360,9 @@ function subjectOf(node: ts.Node): ts.Node { ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && REFLECTION_NAMESPACES.has(callee.expression.text) && - current.arguments.length > 0 + current.arguments.nodes.length > 0 ) { - return current.arguments[0]!; + return current.arguments.nodes[0]!; } return callee; } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index 9ed4ee6fa..e2440b787 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { afterAll, beforeAll, expect, test } from "vitest"; import type { SourceFile, Node } from "typescript/unstable/ast"; -import { AstFile, AstNode } from "./ast-node.js"; +import { AstFile, AstNode, AstNodeList } from "./ast-node.js"; import { AstKind, KIND_NODE_LIST, @@ -229,11 +229,15 @@ for (const name of [...Object.keys(cases), "model.ts"]) { expect(node.getLeadingTriviaWidth()).toBe(expected.getLeadingTriviaWidth()); expect(ids(node.jsDoc)).toEqual(ids(expected.jsDoc)); for (const property of astChildNames(node.kind).split(",").filter(Boolean)) { - const actual = Reflect.get(node, property) as AstNode | AstNode[] | undefined; + const actual = Reflect.get(node, property) as AstNode | AstNodeList | undefined; const reference = Reflect.get(expected, property) as OracleNode | OracleNode[] | undefined; if (Array.isArray(reference)) { - expect(ids(actual as AstNode[]), `${name}:${index}.${property}`).toEqual(ids(reference)); - expect(Reflect.get(node, property)).toBe(actual); + expect(ids((actual as AstNodeList).nodes), `${name}:${index}.${property}`).toEqual( + ids(reference), + ); + expect((Reflect.get(node, property) as AstNodeList).nodes).toBe( + (actual as AstNodeList).nodes, + ); } else { expect((actual as AstNode | undefined)?.index, `${name}:${index}.${property}`).toBe( reference?.index, @@ -256,7 +260,7 @@ for (const name of [...Object.keys(cases), "model.ts"]) { node.forEachChild( () => 0, (list) => { - actualLists.push(list.map((n) => n.index)); + actualLists.push(list.nodes.map((n) => n.index)); return 0; }, ); @@ -322,7 +326,7 @@ test("checker handles reject cross-file, wrong-kind and nil identities", () => { test("direct node slots preserve lazy parents, identity and invalid-index checks", () => { let materialized = 0; - const file = new AstFile(decoded.get("main.ts")!.bytes, undefined, () => { + const file = new AstFile(decoded.get("main.ts")!.bytes, () => { materialized++; }); let index = file.wire.nodeCount - 1; @@ -347,7 +351,7 @@ test("direct node slots preserve lazy parents, identity and invalid-index checks test("named child access shares node and list identities without materializing siblings", () => { let materialized = 0; - const file = new AstFile(decoded.get("main.ts")!.bytes, undefined, () => { + const file = new AstFile(decoded.get("main.ts")!.bytes, () => { materialized++; }); let index = 1; @@ -366,9 +370,11 @@ test("named child access shares node and list identities without materializing s expect(node.child("__proto__")).toBeUndefined(); expect(node.childList("typeParameters")).toBeUndefined(); const parameters = node.childList("parameters"); - expect(parameters?.map((parameter) => parameter.index)).toEqual(file.wire.list(paramsIndex)); - expect(node.child("parameters")).toBe(parameters); - expect(node.parameters).toBe(parameters); + expect(parameters?.nodes.map((parameter) => parameter.index)).toEqual( + file.wire.list(paramsIndex), + ); + expect((node.child("parameters") as AstNodeList | undefined)?.nodes).toBe(parameters?.nodes); + expect(node.parameters?.nodes).toBe(parameters?.nodes); const body = node.childNode("body"); expect(body?.index).toBe(bodyIndex); expect(node.child("body")).toBe(body); @@ -414,7 +420,7 @@ test("a source view requires a source-file root", () => { const nodes = words.getUint32(HEADER_OFFSET_NODES, true); words.setUint32(nodes + NODE_LEN, AstKind.Identifier, true); expect(() => new AstFile(bytes).sourceFile).toThrow("expected a source file root"); - expect(decoded.get("empty.ts")!.file.root.statements).toEqual([]); + expect(decoded.get("empty.ts")!.file.root.statements?.nodes).toEqual([]); }); test("preorder traversal preserves depth, skipped subtrees and early termination", () => { @@ -483,7 +489,7 @@ test("selective preorder preserves semantic order and depth without materializin }; visit(oracle.getOrCreateNodeAtIndex(1), 0); let materialized = 0; - const file = new AstFile(bytes, undefined, () => { + const file = new AstFile(bytes, () => { materialized++; }); const actual: [number, number][] = []; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index d12c199d4..92ab5e4b8 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -16,8 +16,21 @@ import { import type { SourceFile } from "./ast-types.js"; import { SyntaxKind, NodeFlags } from "./enums.js"; +/** A list of child nodes with the list's own position, the shape of tsc-ts's + * NodeList. The file keeps the nodes array, not this object: each read of a + * list field returns a new AstNodeList over the same array, so `nodes` keeps + * its identity and a decoded file holds no list objects. */ +export class AstNodeList { + constructor( + readonly pos: number, + readonly end: number, + readonly nodes: readonly T[], + ) {} +} + /** One immutable AST response owns one object per node and one array per - * list. Parent access is lazy, so resolving a checker handle near the bottom + * list. + * Parent access is lazy, so resolving a checker handle near the bottom * of a deep tree never recursively constructs its ancestors. */ export class AstFile { readonly wire: AstWireFile; @@ -34,7 +47,6 @@ export class AstFile { constructor( bytes: Uint8Array, - private readonly listMetadata?: (nodes: AstNode[], pos: number, end: number) => void, private readonly materialized?: () => void, ) { this.wire = new AstWireFile(bytes); @@ -63,14 +75,15 @@ export class AstFile { return node; } - list(index: number): AstNode[] { - if (Object.hasOwn(this.lists, index)) return this.lists[index]!; - const nodes: AstNode[] = []; - for (const child of this.wire.list(index)) nodes.push(this.node(child)); - this.listMetadata?.(nodes, this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0); - this.lists[index] = nodes; - this.materialized?.(); - return nodes; + list(index: number): AstNodeList { + let nodes = this.lists[index]; + if (nodes === undefined) { + nodes = []; + for (const child of this.wire.list(index)) nodes.push(this.node(child)); + this.lists[index] = nodes; + this.materialized?.(); + } + return new AstNodeList(this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0, nodes); } namedChild(node: AstNode, name: string): number { @@ -190,11 +203,11 @@ export class AstNode { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.node(index); } - childList(name: string): AstNode[] | undefined { + childList(name: string): AstNodeList | undefined { const index = this.file.namedChild(this, name); return index === 0 ? undefined : this.file.list(index); } - child(name: string): AstNode | AstNode[] | undefined { + child(name: string): AstNode | AstNodeList | undefined { const index = this.file.namedChild(this, name); if (index === 0) return undefined; return this.file.wire.kind(index) === KIND_NODE_LIST @@ -213,7 +226,7 @@ export class AstNode { throw new AstDecodeError("sibling belongs to another parent"); const kind = wire.kind(index); if (kind === KIND_NODE_LIST) { - for (const node of file.list(index)) indices.push(node.index); + for (const node of file.list(index).nodes) indices.push(node.index); } else if (kind !== AstKind.JSDoc) { indices.push(index); } @@ -222,7 +235,7 @@ export class AstNode { forEachChild( visitNode: (node: AstNode) => T, - visitList?: (nodes: readonly AstNode[]) => T, + visitList?: (nodes: AstNodeList) => T, ): T | undefined { const wire = this.file.wire; // Walk the immutable sibling links directly. Repeated semantic scans @@ -237,7 +250,7 @@ export class AstNode { const result = visitList(list); if (result) return result; } else { - for (const node of list) { + for (const node of list.nodes) { const result = visitNode(node); if (result) return result; } @@ -395,7 +408,7 @@ export class AstNode { } get modifierFlags(): number { let result = 0; - for (const modifier of this.childList("modifiers") ?? []) + for (const modifier of this.childList("modifiers")?.nodes ?? []) result |= astModifierFlag(modifier.kind); return result; } @@ -439,7 +452,7 @@ export class AstNode { get argumentExpression(): AstNode | undefined { return this.childNode("argumentExpression"); } - get arguments(): readonly AstNode[] | undefined { + get arguments(): AstNodeList | undefined { return this.childList("arguments"); } get assertsModifier(): AstNode | undefined { @@ -448,7 +461,7 @@ export class AstNode { get asteriskToken(): AstNode | undefined { return this.childNode("asteriskToken"); } - get attributes(): AstNode | readonly AstNode[] | undefined { + get attributes(): AstNode | AstNodeList | undefined { return this.child("attributes"); } get awaitModifier(): AstNode | undefined { @@ -466,13 +479,13 @@ export class AstNode { get checkType(): AstNode | undefined { return this.childNode("checkType"); } - get children(): AstNode | readonly AstNode[] | undefined { + get children(): AstNode | AstNodeList | undefined { return this.child("children"); } get className(): AstNode | undefined { return this.childNode("className"); } - get clauses(): readonly AstNode[] | undefined { + get clauses(): AstNodeList | undefined { return this.childList("clauses"); } get closingElement(): AstNode | undefined { @@ -484,7 +497,7 @@ export class AstNode { get colonToken(): AstNode | undefined { return this.childNode("colonToken"); } - get comment(): readonly AstNode[] | undefined { + get comment(): AstNodeList | undefined { return this.childList("comment"); } get condition(): AstNode | undefined { @@ -496,7 +509,7 @@ export class AstNode { get declarationList(): AstNode | undefined { return this.childNode("declarationList"); } - get declarations(): readonly AstNode[] | undefined { + get declarations(): AstNodeList | undefined { return this.childList("declarations"); } get defaultType(): AstNode | undefined { @@ -508,7 +521,7 @@ export class AstNode { get elementType(): AstNode | undefined { return this.childNode("elementType"); } - get elements(): readonly AstNode[] | undefined { + get elements(): AstNodeList | undefined { return this.childList("elements"); } get elseStatement(): AstNode | undefined { @@ -547,7 +560,7 @@ export class AstNode { get head(): AstNode | undefined { return this.childNode("head"); } - get heritageClauses(): readonly AstNode[] | undefined { + get heritageClauses(): AstNodeList | undefined { return this.childList("heritageClauses"); } get importClause(): AstNode | undefined { @@ -574,10 +587,10 @@ export class AstNode { get literal(): AstNode | undefined { return this.childNode("literal"); } - get members(): readonly AstNode[] | undefined { + get members(): AstNodeList | undefined { return this.childList("members"); } - get modifiers(): readonly AstNode[] | undefined { + get modifiers(): AstNodeList | undefined { return this.childList("modifiers"); } get moduleReference(): AstNode | undefined { @@ -622,13 +635,13 @@ export class AstNode { get parameterName(): AstNode | undefined { return this.childNode("parameterName"); } - get parameters(): readonly AstNode[] | undefined { + get parameters(): AstNodeList | undefined { return this.childList("parameters"); } get postfixToken(): AstNode | undefined { return this.childNode("postfixToken"); } - get properties(): readonly AstNode[] | undefined { + get properties(): AstNodeList | undefined { return this.childList("properties"); } get propertyName(): AstNode | undefined { @@ -649,7 +662,7 @@ export class AstNode { get statement(): AstNode | undefined { return this.childNode("statement"); } - get statements(): readonly AstNode[] | undefined { + get statements(): AstNodeList | undefined { return this.childList("statements"); } get tag(): AstNode | undefined { @@ -658,13 +671,13 @@ export class AstNode { get tagName(): AstNode | undefined { return this.childNode("tagName"); } - get tags(): readonly AstNode[] | undefined { + get tags(): AstNodeList | undefined { return this.childList("tags"); } get template(): AstNode | undefined { return this.childNode("template"); } - get templateSpans(): readonly AstNode[] | undefined { + get templateSpans(): AstNodeList | undefined { return this.childList("templateSpans"); } get thenStatement(): AstNode | undefined { @@ -685,7 +698,7 @@ export class AstNode { get type(): AstNode | undefined { return this.childNode("type"); } - get typeArguments(): readonly AstNode[] | undefined { + get typeArguments(): AstNodeList | undefined { return this.childList("typeArguments"); } get typeExpression(): AstNode | undefined { @@ -697,10 +710,10 @@ export class AstNode { get typeParameter(): AstNode | undefined { return this.childNode("typeParameter"); } - get typeParameters(): readonly AstNode[] | undefined { + get typeParameters(): AstNodeList | undefined { return this.childList("typeParameters"); } - get types(): readonly AstNode[] | undefined { + get types(): AstNodeList | undefined { return this.childList("types"); } get value(): AstNode | undefined { diff --git a/packages/compiler/src/frontend/ts7/ast-types.generated.ts b/packages/compiler/src/frontend/ts7/ast-types.generated.ts index 41de9b115..de6ff892f 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.generated.ts @@ -1343,7 +1343,7 @@ export interface JsxText extends Node { export interface SyntaxList extends Node { readonly flags: NodeFlags; readonly kind: SyntaxKind.SyntaxList; - readonly children: readonly Node[]; + readonly children: NodeArray; } export interface JSDoc extends Node { diff --git a/packages/compiler/src/frontend/ts7/ast-types.ts b/packages/compiler/src/frontend/ts7/ast-types.ts index 04af72566..6d421e58d 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.ts @@ -1,4 +1,4 @@ -import type { AstNode } from "./ast-node.js"; +import type { AstNode, AstNodeList } from "./ast-node.js"; import { SyntaxKind } from "./enums.js"; import type { EndOfFile, @@ -11,7 +11,9 @@ import type { export * from "./ast-types.generated.js"; export type Node = AstNode; -export type NodeArray = readonly T[]; +/** A node list. The type is the list class itself, not a structural shape: + * native code represents a class and a record type differently. */ +export type NodeArray = AstNodeList; export type Path = string; export type __String = string; export interface TextRange { diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index c97dbe4c3..ec26aa0b0 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -186,7 +186,7 @@ export function canHaveModifiers(node: Node): boolean { /** The node's modifier tokens, decorators excluded (5.9.3's getModifiers). */ export function getModifiers(node: Node): readonly Modifier[] | undefined { - const modifiers = node.modifiers; + const modifiers = node.modifiers?.nodes; if (modifiers === undefined) return undefined; return modifiers.filter((m): m is Modifier => m.kind !== SyntaxKind.Decorator); } @@ -232,7 +232,7 @@ function modifierToFlag(kind: number): number { } function modifierFlagsOfNode(node: Node): number { - const modifiers = node.modifiers; + const modifiers = node.modifiers?.nodes; let flags = ModifierFlags.None as number; if (modifiers !== undefined) { for (const m of modifiers) flags |= modifierToFlag(m.kind); diff --git a/packages/compiler/src/frontend/ts7/census-check.ts b/packages/compiler/src/frontend/ts7/census-check.ts index 84673720e..1aac06744 100644 --- a/packages/compiler/src/frontend/ts7/census-check.ts +++ b/packages/compiler/src/frontend/ts7/census-check.ts @@ -63,7 +63,7 @@ type CensusTypes = [ ts.SignatureDeclaration, ts.Signature, ts.RegularExpressionLiteral, - ts.NodeArray, + readonly ts.Node[], ts.ForStatement, ts.ExportAssignment, ts.Declaration, diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index 53b960b91..d9b6e44a1 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -52,9 +52,9 @@ function connect(files: Record) { } function declaration(root: SourceFile, name: string): AstNode { - for (const statement of root.statements) { + for (const statement of root.statements.nodes) { if (statement.name?.text === name) return statement; - for (const node of statement.declarationList?.declarations ?? []) + for (const node of statement.declarationList?.declarations?.nodes ?? []) if (node.name?.text === name) return node; } throw new Error(`Missing declaration ${name}`); diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index bf83b367c..f3ef51a3d 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -173,7 +173,7 @@ function isDeferredExecutableRoot(node: Node, walk: PrefetchWalk): boolean { return ( parent.kind === SyntaxKind.PropertyDeclaration && parent.initializer === node && - !parent.modifiers?.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword) + !parent.modifiers?.nodes.some((modifier) => modifier.kind === SyntaxKind.StaticKeyword) ); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index c917a6f66..cb05db50c 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -82,9 +82,9 @@ test("synthetic type roots and descendants match the pinned SDK without claiming const expectedRoot = sdk.program.getSourceFile(file)!; let checked = 0; for (let index = 1; index < 7; index++) { - const statement = root.statements[index]!; + const statement = root.statements.nodes[index]!; const expectedStatement = expectedRoot.statements[index]!; - const node = statement.declarationList!.declarations![0]!.name!; + const node = statement.declarationList!.declarations!.nodes[0]!.name!; const expectedNode = Reflect.get(expectedStatement, "declarationList").declarations[0] .name as SdkNode; const type = project.checker.getTypeAtLocation(node)!; @@ -121,7 +121,7 @@ test("generic, predicate, rest and this-parameter signature fragments match the const root = project.program.getSourceFile(file)!; const expectedRoot = sdk.program.getSourceFile(file)!; for (let index = 7; index < 11; index++) { - const node = root.statements[index]!; + const node = root.statements.nodes[index]!; const expectedNode = expectedRoot.statements[index]!; const signature = project.checker.getSignatureFromDeclaration(node)!; const expectedSignature = sdk.checker.getSignatureFromDeclaration(expectedNode)!; @@ -138,7 +138,7 @@ test("generic, predicate, rest and this-parameter signature fragments match the expect(actual.kind).toBe(SyntaxKind.FunctionType); expect(shape(actual)).toEqual(shape(expected)); expect(actual.file.root).toBe(actual); - const parameters = actual.parameters!; + const parameters = actual.parameters!.nodes; expect(parameters.length).toBeGreaterThan(0); for (const parameter of parameters) { expect(parameter.parent).toBe(actual); diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index 47c6d4a82..ceaceb113 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -288,7 +288,7 @@ test("a released latest snapshot cannot lend stale ASTs to the same project", () source = "export const third = true;"; const third = session.updateSnapshot({ fileChanges: { created: [file] } }); expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(old.statements[0]!.getText()).toBe("export const value = 1;"); + expect(old.statements.nodes[0]!.getText()).toBe("export const value = 1;"); } finally { session.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 12dae3c21..4933da9d6 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -52,7 +52,6 @@ export class Ts7Session { constructor( private readonly rpc: Ts7RpcClient, collectTiming = false, - readonly listMetadata?: (nodes: AstNode[], pos: number, end: number) => void, ) { this.timing = new Ts7Timing(collectTiming); } @@ -280,7 +279,7 @@ export class Ts7SessionProgram { if (source === undefined) { const bytes = this.session.binary("getSourceFile", JSON.stringify(this.query(file))); if (bytes.length !== 0) { - const ast = new AstFile(bytes, this.session.listMetadata, () => { + const ast = new AstFile(bytes, () => { this.session.timing.materialized(); }); this.session.timing.fetched(Math.max(0, ast.wire.nodeCount - 2)); diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts index e416d2920..1fae05ff6 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.test.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts @@ -41,14 +41,14 @@ test("each parse owns its text and nodes across same-path edits, deletion and cl expect(second).not.toBe(first); const other = parser.parse("other.js", "exports.third = 3;", "js"); const empty = parser.parse("input.ts", "", "ts"); - expect(empty.statements).toEqual([]); + expect(empty.statements.nodes).toEqual([]); const restored = parser.parse("input.ts", first.text, "ts"); expect(restored.text).toBe(first.text); parser.close(); parser.close(); - expect(first.statements[0]!.getText()).toBe("export const first = 1;"); - expect(second.statements[0]!.parent).toBe(second); - expect(other.statements[0]!.getText()).toBe("exports.third = 3;"); + expect(first.statements.nodes[0]!.getText()).toBe("export const first = 1;"); + expect(second.statements.nodes[0]!.parent).toBe(second); + expect(other.statements.nodes[0]!.getText()).toBe("exports.third = 3;"); expect(() => parser.parse("late.ts", "", "ts")).toThrow("closed"); }); @@ -113,7 +113,7 @@ for (const { name, kind, source } of syntaxCases) { expect(file.text).toBe(source); expect(file.isDeclarationFile).toBe(oracle.isDeclarationFile); expect( - file.statements.map((statement) => ({ + file.statements.nodes.map((statement) => ({ start: statement.getStart(), end: statement.end, text: statement.getText(), @@ -152,14 +152,16 @@ test("optional declarations expose question tokens without confusing definite as "class A { optional?: number; definite!: number; method?(): void; } type M = { [K in keyof T]-?: T[K] };", "ts", ); - const declaration = file.statements[0]!; + const declaration = file.statements.nodes[0]!; if (!ts.isClassDeclaration(declaration)) throw new Error("expected class"); - expect(declaration.members[0]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); - expect(declaration.members[0]!.questionToken).toBe(declaration.members[0]!.postfixToken); - expect(declaration.members[1]!.questionToken).toBeUndefined(); - expect(declaration.members[1]!.postfixToken?.kind).toBe(ts.SyntaxKind.ExclamationToken); - expect(declaration.members[2]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); - const alias = file.statements[1]!; + expect(declaration.members.nodes[0]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); + expect(declaration.members.nodes[0]!.questionToken).toBe( + declaration.members.nodes[0]!.postfixToken, + ); + expect(declaration.members.nodes[1]!.questionToken).toBeUndefined(); + expect(declaration.members.nodes[1]!.postfixToken?.kind).toBe(ts.SyntaxKind.ExclamationToken); + expect(declaration.members.nodes[2]!.questionToken?.kind).toBe(ts.SyntaxKind.QuestionToken); + const alias = file.statements.nodes[1]!; if (!ts.isTypeAliasDeclaration(alias) || !ts.isMappedTypeNode(alias.type)) throw new Error("expected mapped type"); expect(alias.type.questionToken?.kind).toBe(ts.SyntaxKind.MinusToken); @@ -192,16 +194,16 @@ test("JSDoc callable types cannot replace method body views", () => { true, ts5.ScriptKind.JS, ); - const native = file.statements[0]!; + const native = file.statements.nodes[0]!; const expected = oracle.statements[0]!; if (!ts.isClassDeclaration(native) || !ts5.isClassDeclaration(expected)) throw new Error("expected class"); - expect(native.members.map((member) => member.body?.getText())).toEqual( + expect(native.members.nodes.map((member) => member.body?.getText())).toEqual( expected.members.map((member) => ts5.isMethodDeclaration(member) ? member.body?.getText() : undefined, ), ); - for (const member of native.members) { + for (const member of native.members.nodes) { expect(member.body?.kind).toBe(ts.SyntaxKind.Block); expect(member.body?.parent).toBe(member); } diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index b7fa17b7b..123a5d1a0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2038,7 +2038,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // A GENERIC class's instance type (`Box`) maps to the concrete // INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer // hook owns the instance table (monomorphization by flow). - if (classDecl.typeParameters) { + if (classDecl.typeParameters?.nodes) { const instance = ctx.genericClassInstance?.(classDecl, widened, (argument) => mapType(argument, ctx)) ?? null; @@ -2093,7 +2093,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the instantiation's classval. Uninstantiated `typeof Box` keeps the // type parameter in the construct signature's return — unmapped (the // family has no thunk and no single constructor ABI). - if (classDecl.typeParameters) { + if (classDecl.typeParameters?.nodes) { const ctorSigs = checker.getConstructSignatures(widened); if (ctorSigs.length !== 1) return null; const inst = mapType(checker.getReturnTypeOfSignature(ctorSigs[0]!), ctx); @@ -4299,7 +4299,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null { const sigDecl = checker.signatureDeclaration(sig); const declParams = - sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined; + sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters.nodes : undefined; if ( declParams !== undefined && runtimeParameterCount(declParams) !== sig.getParameters().length @@ -4349,7 +4349,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && construction && ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && + construction.arguments?.nodes[0] === executor && ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && @@ -4869,9 +4869,11 @@ function constAssertedEmptyArrayProp(p: ts.Symbol, ctx: TypeMapperCtx): boolean const init = d.initializer; // `a: [] as const` pins the field type by itself, wherever the literal sits. if (ts.isAsExpression(init) && isConstAssertionTypeNode(init.type)) { - return ts.isArrayLiteralExpression(init.expression) && init.expression.elements.length === 0; + return ( + ts.isArrayLiteralExpression(init.expression) && init.expression.elements.nodes.length === 0 + ); } - if (!ts.isArrayLiteralExpression(init) || init.elements.length > 0) return false; + if (!ts.isArrayLiteralExpression(init) || init.elements.nodes.length > 0) return false; return underConstAssertion(d); } @@ -5041,10 +5043,13 @@ function evalMappedTemplate( .declarationsOf(alias) .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) : undefined; - if (aliasDecl?.typeParameters && aliasArgs.length === aliasDecl.typeParameters.length) { + if ( + aliasDecl?.typeParameters?.nodes && + aliasArgs.length === aliasDecl.typeParameters.nodes.length + ) { scope = new Map(aliases); for (let i = 0; i < aliasArgs.length; i++) { - const param = checker.getSymbolAtLocation(aliasDecl.typeParameters[i]!.name); + const param = checker.getSymbolAtLocation(aliasDecl.typeParameters.nodes[i]!.name); const arg = evalMappedTemplate(aliasArgs[i]!, keys, aliases, ctx); if (!param || !arg) return null; scope.set(param, arg); @@ -5540,7 +5545,7 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null else if ( (ts.isClassDeclaration(owner) || ts.isInterfaceDeclaration(owner)) && owner.name !== undefined && - (owner.typeParameters?.length ?? 0) === 0 + (owner.typeParameters?.nodes.length ?? 0) === 0 ) { const ownerSymbol = checker.getSymbolAtLocation(owner.name); if (ownerSymbol) source = checker.getDeclaredTypeOfSymbol(ownerSymbol); @@ -6211,7 +6216,7 @@ function mapRecordTypeInner( } let writeT: IrType | null = null; if (setDecl) { - const param = (setDecl as ts.SetAccessorDeclaration).parameters[0]; + const param = (setDecl as ts.SetAccessorDeclaration).parameters.nodes[0]; if (!param) return null; writeT = mapType(checker.getTypeAtLocation(param), ctx); // A void/unit-typed write slot has no value form; DIVERGENT @@ -6568,7 +6573,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if ( sigDecl !== undefined && ts.isFunctionLike(sigDecl) && - (runtimeParameterCount(sigDecl.parameters) !== sig.getParameters().length || + (runtimeParameterCount(sigDecl.parameters.nodes) !== sig.getParameters().length || bodyReadsArguments(sigDecl)) ) { return `the function shape is supported, but its signature is variadic ('arguments'-reading), and a compiled signature is fixed-arity`; diff --git a/packages/compiler/src/frontend/type-only-imports.ts b/packages/compiler/src/frontend/type-only-imports.ts index 003d4ccf7..f9eb67a39 100644 --- a/packages/compiler/src/frontend/type-only-imports.ts +++ b/packages/compiler/src/frontend/type-only-imports.ts @@ -30,8 +30,8 @@ export function typeOnlyNamedImport(stmt: ts.ImportDeclaration): boolean { clause.name === undefined && clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings) && - clause.namedBindings.elements.length > 0 && - clause.namedBindings.elements.every((e) => e.isTypeOnly) + clause.namedBindings.elements.nodes.length > 0 && + clause.namedBindings.elements.nodes.every((e) => e.isTypeOnly) ); } @@ -45,7 +45,7 @@ interface ModuleReference { function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] { const refs: ModuleReference[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isImportDeclaration(stmt)) { if (ts.isStringLiteral(stmt.moduleSpecifier)) refs.push({ spec: stmt.moduleSpecifier.text, erased: erasedTypeOnlyImport(stmt) }); @@ -73,7 +73,7 @@ function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] { function callReferencesOf(program: ts.Program, sf: ts.SourceFile): ModuleReference[] { const refs: ModuleReference[] = []; for (const node of moduleSourceCandidates(program, sf).calls) { - const arg = node.arguments[0]; + const arg = node.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) continue; refs.push({ spec: arg.text, diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index 38c3d80a1..c395da593 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -17,10 +17,10 @@ function workerNamespace(program: ts.Program, expression: ts.Expression): boolea ts.isIdentifier(expression.expression) && expression.expression.text === "require" && isNodeGlobal(program, expression.expression) && - expression.arguments.length === 1 && - ts.isStringLiteralLike(expression.arguments[0]!) + expression.arguments.nodes.length === 1 && + ts.isStringLiteralLike(expression.arguments.nodes[0]!) ) - return canonicalBuiltinModule(expression.arguments[0]!.text) === "worker_threads"; + return canonicalBuiltinModule(expression.arguments.nodes[0]!.text) === "worker_threads"; if (!ts.isIdentifier(expression)) return false; if (isBuiltinNamespaceImport(program, expression, "worker_threads")) return true; const checker = program.getTypeChecker(), @@ -122,8 +122,8 @@ export function workerTargetPaths(program: ts.Program, files: readonly ts.Source ); for (const construction of constructions) { const expression = construction.expression; - if (!isWorkerConstructor(program, expression) || !construction.arguments?.[0]) continue; - const path = workerModulePath(program, construction.arguments[0]); + if (!isWorkerConstructor(program, expression) || !construction.arguments?.nodes[0]) continue; + const path = workerModulePath(program, construction.arguments.nodes[0]); if (path !== null) targets.add(workerSourcePath(path)); } } diff --git a/packages/compiler/src/ir/traverse.test.ts b/packages/compiler/src/ir/traverse.test.ts index e72176ef6..874e7846e 100644 --- a/packages/compiler/src/ir/traverse.test.ts +++ b/packages/compiler/src/ir/traverse.test.ts @@ -30,23 +30,23 @@ afterAll(() => program.dispose()); function childType(type: ts.TypeNode): boolean { if (ts.isTypeReferenceNode(type)) return ["IrExpr", "IrStmt"].includes(type.typeName.getText()); if (ts.isArrayTypeNode(type)) return childType(type.elementType); - if (ts.isUnionTypeNode(type)) return type.types.some(childType); + if (ts.isUnionTypeNode(type)) return type.types.nodes.some(childType); if (ts.isTypeLiteralNode(type)) - return type.members.some( + return type.members.nodes.some( (member) => ts.isPropertySignature(member) && member.type && childType(member.type), ); return false; } for (const name of ["IrExpr", "IrStmt"]) { - const alias = source.statements.find( + const alias = source.statements.nodes.find( (node): node is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(node) && node.name.text === name, )!; if (!ts.isUnionTypeNode(alias.type)) throw new Error("IR must be a discriminated union"); - for (const variant of alias.type.types) { + for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IR variants must be records"); - const members = variant.members.filter(ts.isPropertySignature); + const members = variant.members.nodes.filter(ts.isPropertySignature); const tag = members.find((member) => member.name.getText() === "kind")!.type!; const kind = JSON.parse(tag.getText()) as string; test(`${name}.${kind}: every declared child is visited and rewritten, metadata is preserved`, () => { @@ -61,7 +61,8 @@ for (const name of ["IrExpr", "IrStmt"]) { expected.push(node); return node; } - if (ts.isUnionTypeNode(type)) return presence ? value(type.types.find(childType)!) : null; + if (ts.isUnionTypeNode(type)) + return presence ? value(type.types.nodes.find(childType)!) : null; if (ts.isArrayTypeNode(type)) return [value(type.elementType), value(type.elementType)]; if (ts.isTypeLiteralNode(type)) { const result: Record = { @@ -69,7 +70,7 @@ for (const name of ["IrExpr", "IrStmt"]) { drop: true, overflow: true, }; - for (const member of type.members) { + for (const member of type.members.nodes) { if (ts.isPropertySignature(member) && member.type && childType(member.type)) result[member.name.getText()] = value(member.type); } @@ -231,7 +232,7 @@ test("module traversal covers every declared type slot", () => { }; if (name === "SrcLoc") return loc; if (active.has(name)) throw new Error(`unexpected recursive schema reference: ${name}`); - const declaration = source.statements.find( + const declaration = source.statements.nodes.find( (item) => (ts.isInterfaceDeclaration(item) || ts.isTypeAliasDeclaration(item)) && item.name.text === name, @@ -239,21 +240,21 @@ test("module traversal covers every declared type slot", () => { if (!declaration) return undefined; active.add(name); const result = ts.isInterfaceDeclaration(declaration) - ? fields(declaration.members) + ? fields(declaration.members.nodes) : ts.isTypeAliasDeclaration(declaration) ? slots(declaration.type) : undefined; active.delete(name); return result; } - if (ts.isTypeLiteralNode(node)) return fields(node.members); + if (ts.isTypeLiteralNode(node)) return fields(node.members.nodes); if (ts.isArrayTypeNode(node)) return [slots(node.elementType)]; if (ts.isParenthesizedTypeNode(node)) return slots(node.type); - if (ts.isIntersectionTypeNode(node)) return slots(node.types[0]!); + if (ts.isIntersectionTypeNode(node)) return slots(node.types.nodes[0]!); if (ts.isUnionTypeNode(node)) { // Metadata unions can choose any arm: the schema puts executable and // typed alternatives first, followed by null/unit alternatives. - return slots(node.types[0]!); + return slots(node.types.nodes[0]!); } return undefined; } @@ -271,11 +272,14 @@ test("module traversal covers every declared type slot", () => { } return result; } - const declaration = source.statements.find( + const declaration = source.statements.nodes.find( (item): item is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(item) && item.name.text === "IrModule", )!; - const module = { ...fields(declaration.members), sourceFile: loc.file } as unknown as IrModule; + const module = { + ...fields(declaration.members.nodes), + sourceFile: loc.file, + } as unknown as IrModule; const seen: IrType[] = []; expect( everyModuleNode(module, { @@ -303,14 +307,14 @@ test("module traversal covers every declared type slot", () => { }); test("type traversal visits all structural children and leaves named shapes as references", () => { - const alias = source.statements.find( + const alias = source.statements.nodes.find( (node): node is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(node) && node.name.text === "IrType", )!; if (!ts.isUnionTypeNode(alias.type)) throw new Error("IrType must be a union"); - for (const variant of alias.type.types) { + for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IrType variants must be records"); - const members = variant.members.filter(ts.isPropertySignature); + const members = variant.members.nodes.filter(ts.isPropertySignature); const kind = JSON.parse( members.find((member) => member.name.getText() === "kind")!.type!.getText(), ) as string; diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts index 610a389f1..95fc28647 100644 --- a/packages/compiler/src/wasm/discover.ts +++ b/packages/compiler/src/wasm/discover.ts @@ -182,13 +182,13 @@ class Discovery { const dir = dirname(sf.fileName); if (ts.isCallExpression(node)) { const callee = node.expression; - const first = node.arguments[0]; + const first = node.arguments.nodes[0]; if (first === undefined) return null; // require.resolve / createRequire(...).resolve / import.meta.resolve if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") { const specifier = wasmLiteral(first); if (specifier === null) { - return this.joinedPath(dir, node.arguments); + return this.joinedPath(dir, node.arguments.nodes); } const receiver = callee.expression; if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) { @@ -206,14 +206,14 @@ class Discovery { (callee.name.text === "join" || callee.name.text === "resolve")) || (ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve")) ) { - return this.joinedPath(dir, node.arguments); + return this.joinedPath(dir, node.arguments.nodes); } return null; } if (ts.isNewExpression(node)) { // new URL("./x.wasm", import.meta.url) if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null; - const args = node.arguments; + const args = node.arguments?.nodes; if (args === undefined || args.length !== 2) return null; const specifier = wasmLiteral(args[0]!); const base = args[1]!; @@ -339,7 +339,7 @@ function isRequireLike(node: ts.Expression): boolean { /** The bytes of an array literal of integer literals, or null. */ function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null { - const elements = node.elements; + const elements = node.elements.nodes; const out = new Uint8Array(elements.length); for (let i = 0; i < elements.length; i++) { const element = elements[i]!; diff --git a/packages/compiler/test/ts7/facade.test.ts b/packages/compiler/test/ts7/facade.test.ts index 4b092a858..94bab8b31 100644 --- a/packages/compiler/test/ts7/facade.test.ts +++ b/packages/compiler/test/ts7/facade.test.ts @@ -381,15 +381,15 @@ void top; const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const functions = sf.statements.filter(ad.isFunctionDeclaration); - const cls = sf.statements.find(ad.isClassDeclaration)!; + const functions = sf.statements.nodes.filter(ad.isFunctionDeclaration); + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; const withClass = functions[2]!; - const nested = withClass.body!.statements.find(ad.isClassDeclaration)!; + const nested = withClass.body!.statements.nodes.find(ad.isClassDeclaration)!; const reachedBody = functions[0]!.body!; const deadBody = functions[1]!.body!; - const nestedMethodBody = nested.members.find(ad.isMethodDeclaration)!.body!; - const defaultValue = functions[0]!.parameters[0]!.initializer!; - const fieldValue = cls.members.find(ad.isPropertyDeclaration)!.initializer!; + const nestedMethodBody = nested.members.nodes.find(ad.isMethodDeclaration)!.body!; + const defaultValue = functions[0]!.parameters.nodes[0]!.initializer!; + const fieldValue = cls.members.nodes.find(ad.isPropertyDeclaration)!.initializer!; const inside = (node: Node, root: Node): boolean => node.getStart() >= root.getStart() && node.end <= root.end; @@ -445,7 +445,7 @@ const item: Alias = identity({ value: 1 }); const { proxy, counts, calls } = countingChecker(raw); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const fn = sf.statements.find(ad.isFunctionDeclaration)!; + const fn = sf.statements.nodes.find(ad.isFunctionDeclaration)!; const references: Node[] = []; ad.walkPreorder(sf, (node) => { if (ad.isTypeReferenceNode(node)) references.push(node.typeName); @@ -458,8 +458,8 @@ const item: Alias = identity({ value: 1 }); calls[method]!.flatMap(([nodes]) => (Array.isArray(nodes) ? nodes : [nodes])) as Node[]; expect(queried("getTypeAtLocation").some((node) => references.includes(node))).toBe(false); expect(queried("getSymbolAtLocation").some((node) => references.includes(node))).toBe(false); - const child = sf.statements.filter(ad.isClassDeclaration)[1]!; - const base = child.heritageClauses![0]!.types[0]!.expression; + const child = sf.statements.nodes.filter(ad.isClassDeclaration)[1]!; + const base = child.heritageClauses!.nodes[0]!.types.nodes[0]!.expression; expect(queried("getTypeAtLocation")).toContain(base); const reference = references[0]!; @@ -485,8 +485,8 @@ test("declaration misses stay direct while explicit whole-file prefetch remains const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; expect(sf.isDeclarationFile).toBe(true); - const declaration = sf.statements.find(ad.isInterfaceDeclaration)!; - const name = declaration.members.find(ad.isPropertySignature)!.name; + const declaration = sf.statements.nodes.find(ad.isInterfaceDeclaration)!; + const name = declaration.members.nodes.find(ad.isPropertySignature)!.name; expect(facade.getSymbolAtLocation(name)).toBe(raw.getSymbolAtLocation(name)); expect(facade.getTypeAtLocation(name)).toBe(raw.getTypeAtLocation(name)); expect(calls["getSymbolAtLocation"]).toEqual([[name]]); @@ -517,7 +517,7 @@ export function dead(input: number): number { const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); @@ -551,7 +551,7 @@ ${locals} const { proxy, counts, calls } = countingChecker(w.p7.project.checker); const facade = new CheckerFacade(proxy); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); @@ -600,8 +600,8 @@ console.log("ok"); const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const cls = sf.statements.find(ad.isClassDeclaration)!; - const ctor = cls.members.find(ad.isConstructorDeclaration)!; + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; + const ctor = cls.members.nodes.find(ad.isConstructorDeclaration)!; const insideCtor = (node: Node): boolean => node.getStart() >= ctor.getStart() && node.end <= ctor.end; @@ -630,9 +630,9 @@ test("signature collection batches exact types of deferred function defaults", ( const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const initializers = sf.statements + const initializers = sf.statements.nodes .filter(ad.isFunctionDeclaration) - .map((decl) => decl.parameters[0]!.initializer!); + .map((decl) => decl.parameters.nodes[0]!.initializer!); lowerToIr(w.p7, sf, [sf]); @@ -665,10 +665,10 @@ test("class-shape collection batches deferred method default types", () => { const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const cls = sf.statements.find(ad.isClassDeclaration)!; - const initializers = cls.members + const cls = sf.statements.nodes.find(ad.isClassDeclaration)!; + const initializers = cls.members.nodes .filter(ad.isMethodDeclaration) - .map((member) => member.parameters[0]!.initializer!); + .map((member) => member.parameters.nodes[0]!.initializer!); lowerToIr(w.p7, sf, [sf]); @@ -703,7 +703,7 @@ console.log(pick(42)); const facade = new CheckerFacade(proxy, { project: w.p7.project.checker.project }); (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const fn = sf.statements.find(ad.isFunctionDeclaration)!; + const fn = sf.statements.nodes.find(ad.isFunctionDeclaration)!; const insideBody = (node: Node): boolean => node.getStart() >= fn.body!.getStart() && node.end <= fn.body!.end; @@ -747,7 +747,7 @@ console.log(reached(1)); // lowering and this assertion observe the same memo/batch traffic. (w.p7 as unknown as { checkerFacade: CheckerFacade | null }).checkerFacade = facade; const sf = w.p7.getSourceFile(w.files[0]!)!; - const deadBody = sf.statements + const deadBody = sf.statements.nodes .filter(ad.isFunctionDeclaration) .find((decl) => decl.name?.text === "dead")!.body!; const insideDeadBody = (node: Node): boolean => @@ -799,7 +799,7 @@ export function f(input: number): number { ); worlds.push(w); const sf = w.p7.getSourceFile(w.files[0]!)!; - const body = sf.statements.find(ad.isFunctionDeclaration)!.body!; + const body = sf.statements.nodes.find(ad.isFunctionDeclaration)!.body!; const identifiers: Node[] = []; ad.walkPreorder(body, (node) => { if (ad.isIdentifier(node)) identifiers.push(node); diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts index e7479c749..f76de2f8c 100644 --- a/packages/compiler/test/ts7/program-adapter.test.ts +++ b/packages/compiler/test/ts7/program-adapter.test.ts @@ -69,7 +69,7 @@ test("retains source-file BOM stripping", () => { const load = loadProgram(entry); try { - expect(load.entry.statements[0]?.getStart(load.entry)).toBe(0); + expect(load.entry.statements.nodes[0]?.getStart(load.entry)).toBe(0); expect(load.entry.text).toBe("const value = 1;\n"); } finally { load.dispose(); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index e363567c2..d323bd901 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -88,23 +88,27 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve()); const declaration = symbol!.declarations[0]!.resolve()!; expect(declaration).toBeInstanceOf(AstNode); - const variable = source - .statements[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; - expect(variable.declarationList.declarations[0]).toBe(declaration); + const variable = source.statements + .nodes[0]! as import("../../src/frontend/ts7/ast-types.js").VariableStatement; + expect(variable.declarationList.declarations.nodes[0]).toBe(declaration); expect( - project.checker.getSymbolAtLocation(variable.declarationList.declarations[0]!.name), + project.checker.getSymbolAtLocation(variable.declarationList.declarations.nodes[0]!.name), ).toBe(symbol); - expect(project.checker.getTypeAtLocation(variable.declarationList.declarations[0]!.name)).toBe( - first, - ); + expect( + project.checker.getTypeAtLocation(variable.declarationList.declarations.nodes[0]!.name), + ).toBe(first); const expectedSource = expected.getProject(config)!.program.getSourceFile(path)!; - expect(source.statements.map((node) => [node.kind, node.pos, node.end])).toEqual( + expect(source.statements.nodes.map((node) => [node.kind, node.pos, node.end])).toEqual( expectedSource.statements.map((node) => [node.kind, node.pos, node.end]), ); - // Native arrays retain element identity; SDK-only properties no longer - // need a JavaScript Object.assign adapter on production source lists. - expect(Array.isArray(source.statements)).toBe(true); - expect(Object.hasOwn(source.statements, "pos")).toBe(false); + // A list carries its own position, as tsc-ts's NodeList does, and its + // nodes are a plain array that retains element identity. + expect([source.statements.pos, source.statements.end]).toEqual([ + expectedSource.statements.pos, + expectedSource.statements.end, + ]); + expect(Array.isArray(source.statements.nodes)).toBe(true); + expect(Object.hasOwn(source.statements.nodes, "pos")).toBe(false); expect(Object.hasOwn(source.statements, "transformFlags")).toBe(false); expect(source.referencedFiles).toBe(source.referencedFiles); expect(source.imports).toBe(source.imports); @@ -180,6 +184,10 @@ test("native semantic registries preserve project scope and snapshot lifetimes", writeFileSync(path, source); const api = new Ts7Api({ cwd: dir, fs: fallback }); const sdk = new API({ cwd: dir }); + // scriptc's node lists keep their nodes in `nodes`; the SDK's lists are arrays. + type SdkNode = import("typescript/unstable/ast").Node; + const nodesOf = (list: unknown): readonly SdkNode[] => + Array.isArray(list) ? list : (list as { nodes: readonly SdkNode[] }).nodes; function facts(snapshot: Snapshot) { const a = snapshot.getProject(firstConfig)!; const b = snapshot.getProject(secondConfig)!; @@ -190,8 +198,8 @@ test("native semantic registries preserve project scope and snapshot lifetimes", const typeB = b.checker.getTypeAtPosition(path, answer)!; const fileA = a.program.getSourceFile(path)!; const fileB = b.program.getSourceFile(path)!; - const signatureA = a.checker.getSignatureFromDeclaration(fileA.statements[0]!)!; - const signatureB = b.checker.getSignatureFromDeclaration(fileB.statements[0]!)!; + const signatureA = a.checker.getSignatureFromDeclaration(nodesOf(fileA.statements)[0]!)!; + const signatureB = b.checker.getSignatureFromDeclaration(nodesOf(fileB.statements)[0]!)!; return { shape: { sharedSymbol: symbolA === symbolB, @@ -200,8 +208,10 @@ test("native semantic registries preserve project scope and snapshot lifetimes", sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(), explicitDeclaration: symbolB.declarations[0]!.resolve(b) === - (fileB.statements[1] as import("typescript/unstable/ast").VariableStatement) - .declarationList.declarations[0], + nodesOf( + (nodesOf(fileB.statements)[1] as import("typescript/unstable/ast").VariableStatement) + .declarationList.declarations, + )[0], texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)], parameters: signatureA.getParameters().map((symbol) => symbol.name), }, diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index dfb811450..45a01c213 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -277,12 +277,12 @@ const getters = names const match = declarations.match(new RegExp(`get ${name}\\(\\): ([^;]+);`)); if (!match) throw new Error(`Missing AST child declaration: ${name}`); const type = match[1] - .replaceAll("RemoteNodeList", "readonly AstNode[]") + .replaceAll("RemoteNodeList", "AstNodeList") .replaceAll("RemoteNode", "AstNode"); const method = - type.includes("AstNode |") && type.includes("AstNode[]") + type.includes("AstNode |") && type.includes("AstNodeList") ? "child" - : type.includes("AstNode[]") + : type.includes("AstNodeList") ? "childList" : "childNode"; return ` get ${name}(): ${type} {\n return this.${method}(${JSON.stringify(name)});\n }`; diff --git a/scripts/generate-ts7-ast-views.mjs b/scripts/generate-ts7-ast-views.mjs index 3b5d3d66f..1525f6246 100644 --- a/scripts/generate-ts7-ast-views.mjs +++ b/scripts/generate-ts7-ast-views.mjs @@ -63,6 +63,8 @@ export function generateAstViews(root, packageRoot, version, check) { // The pinned native wire and RemoteNode getter encode one tag, despite // the SDK factory-oriented interface declaring an array of tags. if (key === "jsdocPropertyTags") type = "JSDocPropertyTag"; + // A list's children are a node list, as every other list field. + if (name === "SyntaxList" && key === "children") type = "NodeArray"; if (member.questionToken) type += " | undefined"; fields.set(key, ` readonly ${key}: ${type};`); } diff --git a/tests/fixtures/self-hosting/frontend-services-cases.ts b/tests/fixtures/self-hosting/frontend-services-cases.ts index 00bdcd230..0a28c740c 100644 --- a/tests/fixtures/self-hosting/frontend-services-cases.ts +++ b/tests/fixtures/self-hosting/frontend-services-cases.ts @@ -41,10 +41,10 @@ export function runFrontendServices(services: FrontendServices, input: string, o const before = host.createProgram([sourcePath], { noLib: true, types: [] }); try { const source = before.getSourceFile(sourcePath)!; - const statement = source.statements[0]!; + const statement = source.statements.nodes[0]!; check(isVariableStatement(statement), "native statement view"); if (isVariableStatement(statement)) { - const declaration = statement.declarationList.declarations[0]!; + const declaration = statement.declarationList.declarations.nodes[0]!; const name = declaration.name; check(isIdentifier(name), "native binding view"); if (isIdentifier(name)) { diff --git a/tests/fixtures/self-hosting/ts7-ast-views.ts b/tests/fixtures/self-hosting/ts7-ast-views.ts index 6f19cda1d..10f48485d 100644 --- a/tests/fixtures/self-hosting/ts7-ast-views.ts +++ b/tests/fixtures/self-hosting/ts7-ast-views.ts @@ -8,8 +8,8 @@ import { SyntaxKind } from "../../../packages/compiler/src/frontend/ts7/enums.js function check(value: boolean, message: string): void { if (!value) throw new Error(message); } function checkStatements(nodes: NodeArray, source: SourceFile): void { - check(nodes === source.statements, "typed statement array identity"); - for (const statement of nodes) { + check(nodes.nodes === source.statements.nodes, "typed statement list nodes identity"); + for (const statement of nodes.nodes) { check(statement.parent === source && statement.getSourceFile() === source, "typed source identity"); check(ts.isStatement(statement), "statement kind predicate"); } @@ -38,14 +38,14 @@ export function checkAstViews(file: AstFile): void { // one AstNode; reads must use its original lazy getters and caches. const body = (node as { body?: Node }).body; const name = (node as { name?: Node })["name"]; - const flags = (node as { modifiers?: readonly Node[] }).modifiers; + const flags = (node as { modifiers?: NodeArray }).modifiers; check(body === node.body && name === node.name, "structural child getter identity"); - check(flags === node.modifiers, "structural list getter identity"); + check(flags?.nodes === node.modifiers?.nodes, "structural list getter nodes identity"); if (body !== undefined) { bodies++; check(body.parent === node, "structural body parent"); } if (name !== undefined) { names++; check(name.parent === node, "structural name parent"); } if (flags !== undefined) { - modifiers += flags.length; - for (const modifier of flags) check(modifier.parent === node, "structural modifier parent"); + modifiers += flags.nodes.length; + for (const modifier of flags.nodes) check(modifier.parent === node, "structural modifier parent"); } if (ts.isIdentifier(node)) { identifiers += node.text.length; @@ -54,16 +54,16 @@ export function checkAstViews(file: AstFile): void { } if (ts.isStringLiteral(node)) strings += node.text.toUpperCase().length; if (ts.isVariableDeclarationList(node)) { - declarations += node.declarations.length; - for (const declaration of node.declarations) check(declaration.parent === node, "declaration identity"); + declarations += node.declarations.nodes.length; + for (const declaration of node.declarations.nodes) check(declaration.parent === node, "declaration identity"); } if (ts.isClassDeclaration(node)) { - for (const member of node.members) { - if (ts.isMethodDeclaration(member)) methods += member.parameters.length; + for (const member of node.members.nodes) { + if (ts.isMethodDeclaration(member)) methods += member.parameters.nodes.length; } } if (ts.isCallExpression(node)) { - calls += node.arguments.length; + calls += node.arguments.nodes.length; check(node.expression.parent === node, "call callee identity"); } if (ts.isBinaryExpression(node)) { diff --git a/tests/fixtures/self-hosting/ts7-checker-cases.ts b/tests/fixtures/self-hosting/ts7-checker-cases.ts index 6a2627e3c..52cb6981e 100644 --- a/tests/fixtures/self-hosting/ts7-checker-cases.ts +++ b/tests/fixtures/self-hosting/ts7-checker-cases.ts @@ -21,9 +21,9 @@ function check(value: boolean, message: string): void { } function declaration(root: SourceFile, name: string): AstNode { - for (const statement of root.statements) { + for (const statement of root.statements.nodes) { if (statement.name?.text === name) return statement; - for (const variable of statement.declarationList?.declarations ?? []) { + for (const variable of statement.declarationList?.declarations?.nodes ?? []) { if (variable.name?.text === name) return variable; } } @@ -87,12 +87,12 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.signatureDeclaration(resolved) === identity, "instantiated signature declaration"); check(facade.typeToString(facade.getReturnTypeOfSignature(resolved)).includes("n: number"), "instantiated return type"); - const ref = declaration(root, "ref").initializer!.properties![0]!; + const ref = declaration(root, "ref").initializer!.properties?.nodes[0]!; check(facade.getShorthandAssignmentValueSymbol(ref) === answerSymbol, "shorthand value symbol"); - const renamed = root.statements.find((statement) => statement.kind === SyntaxKind.ExportDeclaration)!.exportClause!.elements![0]!; + const renamed = root.statements.nodes.find((statement) => statement.kind === SyntaxKind.ExportDeclaration)!.exportClause!.elements?.nodes[0]!; const alias = facade.getSymbolAtLocation(renamed.name!)!; check(facade.getAliasedSymbol(alias) === answerSymbol, "export alias"); - const member = declaration(root, "Mode").members![0]!; + const member = declaration(root, "Mode").members?.nodes[0]!; check(facade.getConstantValue(member) === 3, "enum constant"); check(facade.getConstantValue(answer) === undefined, "absent constant"); const contextual = declaration(root, "contextual").initializer!; @@ -173,7 +173,7 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque batch.prefetchSymbolRoots([identity, identity]); batch.prefetchSymbolNodesExact([answerName, answerName]); batch.prefetchCollectionTypes([call, call]); - batch.prefetchClassCollection([box.members![0]!.initializer!], [box]); + batch.prefetchClassCollection([box.members?.nodes[0]!.initializer!], [box]); batch.prefetchRoots([identity.body!, identity.body!]); batch.prefetchSourceFile(root); check(batch.getTypeAtLocation(answerName) === answerType && batch.getSymbolAtLocation(answerName) === answerSymbol, "prefetched semantic identity"); diff --git a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts index db57b95f2..949eff4b7 100644 --- a/tests/fixtures/self-hosting/ts7-checker-snapshots.ts +++ b/tests/fixtures/self-hosting/ts7-checker-snapshots.ts @@ -14,7 +14,7 @@ function protocolPath(path: string): string { } function exportedName(source: SourceFile) { - return source.statements[1]!.declarationList!.declarations![0]!.name!; + return source.statements.nodes[1]!.declarationList!.declarations?.nodes[0]!.name!; } /** Reuse one server, one source version and both old/new semantic graphs. diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index 2aba713c3..c98869785 100644 --- a/tests/fixtures/self-hosting/ts7-client-cases.ts +++ b/tests/fixtures/self-hosting/ts7-client-cases.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { Ts7RpcClient } from "../../../packages/compiler/src/frontend/ts7/rpc-client.js"; import { registerTs7FileSystem } from "../../../packages/compiler/src/frontend/ts7/rpc-filesystem.js"; import { Ts7Wire } from "../../../packages/compiler/src/frontend/ts7/rpc-wire.js"; -import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js"; +import { AstFile, AstNode, AstNodeList } from "../../../packages/compiler/src/frontend/ts7/ast-node.js"; import { AstKind, KIND_NODE_LIST, astChildNames } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js"; import { decodeAstString } from "../../../packages/compiler/src/frontend/ts7/ast-bytes.js"; import { SyntaxKind, ScriptTarget, ModuleKind, ModuleResolutionKind, ModuleDetectionKind, syntaxKindName, scriptTargetName, moduleKindName, moduleResolutionKindName, moduleDetectionKindName } from "../../../packages/compiler/src/frontend/ts7/enums.js"; @@ -56,7 +56,7 @@ function checkSemanticSurrogateBoundary(): string { function checkAst(ast: AstFile, source: string): void { check(ast.root.text === source, "decoded source text"); check(ast.root.getSourceFile() === ast.root, "source identity"); - check(ast.root.statements === ast.root.statements, "stable child list"); + check(ast.root.statements?.nodes === ast.root.statements?.nodes, "stable child list nodes"); for (let index = 1; index < ast.wire.nodeCount; index++) { if (ast.wire.kind(index) === KIND_NODE_LIST) { ast.list(index); continue; } const node = ast.node(index); @@ -70,15 +70,15 @@ function checkAst(ast: AstFile, source: string): void { check(node.getFullStart() === node.pos && node.getEnd() === node.end, "span endpoints"); for (const name of astChildNames(node.kind).split(",")) { const child = node.child(name!); - if (Array.isArray(child)) { - for (const item of child) check(item.parent === node, "list child parent"); + if (child instanceof AstNodeList) { + for (const item of child.nodes) check(item.parent === node, "list child parent"); } else if (child !== undefined) check(child.parent === node, "named child parent"); } const seen: AstNode[] = []; node.forEachChild((child) => { seen.push(child); }); check(node.forEachChild((child) => child.index) === seen[0]?.index, "visitor early return"); let listElements = 0; - node.forEachChild(() => {}, (list) => { listElements += list.length; }); + node.forEachChild(() => {}, (list) => { listElements += list.nodes.length; }); check(listElements <= seen.length, "array visitor"); for (const doc of node.jsDoc ?? []) check(doc.kind === AstKind.JSDoc, "JSDoc children"); } @@ -249,17 +249,17 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st check(project.program.getSourceFileMetadata(file)!.isDefaultLibrary === false, "source metadata"); check(!project.program.isSourceFileFromExternalLibrary(root) && !project.program.isSourceFileDefaultLibrary(root), "source library flags"); checkAst(tree, content); - const statements = tree.root.statements!; - check(statements[1]!.declarationList!.declarations![0]!.initializer!.text === "\uFEFFhéllo 🌍", "string BOM retained"); + const statements = tree.root.statements!.nodes; + check(statements[1]!.declarationList!.declarations!.nodes[0]!.initializer!.text === "\uFEFFhéllo 🌍", "string BOM retained"); const box = statements[2]!; - check(box.getStart(undefined, true) < box.getStart() && box.members!.length === 2, "class JSDoc and members"); - check(box.members![0]!.modifierFlags !== 0, "readonly modifier flag"); - check(statements[3]!.declarationList!.declarations![0]!.initializer!.elements!.length === 40, "long native node list"); - const template = statements[4]!.declarationList!.declarations![0]!.initializer!; + check(box.getStart(undefined, true) < box.getStart() && box.members!.nodes.length === 2, "class JSDoc and members"); + check(box.members!.nodes[0]!.modifierFlags !== 0, "readonly modifier flag"); + check(statements[3]!.declarationList!.declarations!.nodes[0]!.initializer!.elements!.nodes.length === 40, "long native node list"); + const template = statements[4]!.declarationList!.declarations!.nodes[0]!.initializer!; check(template.head!.text === "head\n" && template.head!.rawText === "head\\n", "cooked and raw template text"); check(project.program.getSemanticDiagnostics(file).length === 0, "valid program diagnostics"); check(project.program.getSyntacticDiagnostics(file).length === 0 && project.program.getBindDiagnostics(file).length === 0, "parse and bind diagnostics"); - const declaration = root.statements![0]!.declarationList!.declarations![0]!; + const declaration = root.statements.nodes[0]!.declarationList!.declarations?.nodes[0]!; const identifier = declaration.name!; const checker = project.checker; const ownedType = checker.getTypeAtLocation(identifier)!; diff --git a/tests/fixtures/self-hosting/ts7-program-host-cases.ts b/tests/fixtures/self-hosting/ts7-program-host-cases.ts index de51d1fa2..44fb29a3b 100644 --- a/tests/fixtures/self-hosting/ts7-program-host-cases.ts +++ b/tests/fixtures/self-hosting/ts7-program-host-cases.ts @@ -60,10 +60,10 @@ export function runProgramHost(createHost: (options: Ts7HostOptions) => Ts7Host, check(program.getSourceFiles() === program.getSourceFiles(), "source file cache"); const checker = program.getTypeChecker(); check(checker === program.getTypeChecker(), "checker cache"); - const statement = source.statements[1]!; + const statement = source.statements.nodes[1]!; check(isVariableStatement(statement), "variable statement"); if (isVariableStatement(statement)) { - const name = statement.declarationList.declarations[0]!.name; + const name = statement.declarationList.declarations.nodes[0]!.name; check(name !== undefined && isIdentifier(name), "variable identifier"); if (name !== undefined && isIdentifier(name)) { const type = checker.getTypeAtLocation(name); diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index b2538c119..3fd34cefd 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -37,9 +37,9 @@ function check(value: boolean, message: string): void { } function declaration(root: AstNode, name: string): AstNode { - for (const statement of root.statements ?? []) { + for (const statement of root.statements?.nodes ?? []) { if (statement.name?.text === name) return statement; - for (const variable of statement.declarationList?.declarations ?? []) { + for (const variable of statement.declarationList?.declarations?.nodes ?? []) { if (variable.name?.text === name) return variable; } } @@ -135,7 +135,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const dict = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Dict").name!)); const index = checker.getIndexInfosOfType(dict)[0]!; check(index.keyType === intrinsicString && index.valueType === intrinsicNumber && index.isReadonly, "index signature types"); - check(index.declaration!.resolve() === declaration(root, "Dict").members![0], "index signature declaration"); + check(index.declaration!.resolve() === declaration(root, "Dict").members?.nodes[0], "index signature declaration"); const generic = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Generic").name!)); const genericParameter = generic.getTypeParameters()[0]!; check(genericParameter.isTypeParameter() && generic.getLocalTypeParameters()[0] === genericParameter && generic.getOuterTypeParameters().length === 0, "interface type parameters"); @@ -147,7 +147,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const signature = signatureAt(checker, identity); const signatureTypes = signature.getTypeParameters(); check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters"); - check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters![0], "signature parameters"); + check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarations[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); check(signature.declaration!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); @@ -177,11 +177,11 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(!checker.isUnknownSymbol(answerSymbol) && !checker.isUndefinedSymbol(answerSymbol) && !checker.isArgumentsSymbol(answerSymbol), "well-known symbol exclusion"); const undefinedSymbol = checker.resolveName("undefined", SemanticSymbolFlags.Value, answerName)!; check(checker.isUndefinedSymbol(undefinedSymbol), "well-known undefined symbol"); - const shorthand = declaration(root, "ref").initializer!.properties![0]!; + const shorthand = declaration(root, "ref").initializer!.properties?.nodes[0]!; check(checker.getShorthandAssignmentValueSymbol(shorthand) === answerSymbol, "shorthand symbol"); const exportSpecifier = renamed.declarations[0]!.resolve()!; check(checker.getExportSpecifierLocalTargetSymbol(exportSpecifier) === answerSymbol, "export specifier symbol"); - check(checker.getConstantValue(declaration(root, "Mode").members![0]!) === 3, "enum constant"); + check(checker.getConstantValue(declaration(root, "Mode").members?.nodes[0]!) === 3, "enum constant"); check(checker.getConstantValue(answerName) === undefined, "missing constant"); check(checker.getContextualType(declaration(root, "pair").initializer!) === pair, "contextual tuple type"); check(checker.getTypeFromTypeNode(declaration(root, "Choice").type!) === choice, "type-node query identity"); @@ -206,7 +206,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(syntheticType.file.node(1) === syntheticType, "synthetic root identity"); const syntheticSignature = checker.signatureToSignatureDeclaration(signature, AstKind.FunctionType, identity)!; check(syntheticSignature.kind === AstKind.FunctionType, "binary signature node"); - check(syntheticSignature.parameters!.length === 1 && syntheticSignature.parameters![0]!.name!.text === "value", "synthetic signature children"); + check(syntheticSignature.parameters?.nodes.length === 1 && syntheticSignature.parameters?.nodes[0]!.name!.text === "value", "synthetic signature children"); let syntheticRefusals = 0; try { syntheticType.getSourceFile(); } catch { syntheticRefusals++; } try { syntheticSignature.file.sourceFile; } catch { syntheticRefusals++; } @@ -218,7 +218,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(usages.length === 1 && usages[0]!.name.resolve()!.text === "identity" && usages[0]!.call!.resolve() === declaration(root, "called").initializer, "signature usage handles"); const completions = checker.getCompletionsAtPosition(root.fileName, root.text!.indexOf("Mode.Ready") + 5, { includeSymbol: true })!; const ready = completions.entries.find((entry) => entry.name === "Ready")!; - check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members![0]!.name!), "completion symbol identity"); + check(ready.symbol === symbolAt(checker, declaration(root, "Mode").members?.nodes[0]!.name!), "completion symbol identity"); check(checker.getCompletionsAtPosition(root.fileName, answerName.getStart()) === undefined, "absent completions"); check(answerType.flags === SemanticTypeFlags.NumberLiteral, "literal flags pin"); check(checkSemanticRefinements(answerType) === "number:43", "native number literal refinement"); diff --git a/tests/fixtures/self-hosting/ts7-source-parser-cases.ts b/tests/fixtures/self-hosting/ts7-source-parser-cases.ts index 7bf796dd5..8a1fd23d8 100644 --- a/tests/fixtures/self-hosting/ts7-source-parser-cases.ts +++ b/tests/fixtures/self-hosting/ts7-source-parser-cases.ts @@ -32,7 +32,7 @@ export function runSourceParser(parser: Ts7SourceParser, input: string, output: nodes, optional, declaration: file.isDeclarationFile, - statements: file.statements.map((statement) => ({ + statements: file.statements.nodes.map((statement) => ({ text: statement.getText(), start: statement.getStart(), end: statement.end, })), imports: moduleSpecifiersOfFile(file), @@ -98,7 +98,7 @@ export function runSourceParser(parser: Ts7SourceParser, input: string, output: } parser.close(); parser.close(); - const versions = [original.statements[0]!.getText(), updated.statements[0]!.getText()]; + const versions = [original.statements.nodes[0]!.getText(), updated.statements.nodes[0]!.getText()]; let closed = false; try { parser.parse("late.ts", "", "ts"); } catch { closed = true; } writeFileSync(output, JSON.stringify({ files, packages, versions, closed })); diff --git a/tests/harness/stdlib-surface.test.ts b/tests/harness/stdlib-surface.test.ts index e91e7be4d..53c2704bf 100644 --- a/tests/harness/stdlib-surface.test.ts +++ b/tests/harness/stdlib-surface.test.ts @@ -97,9 +97,9 @@ const probeErrors = ts .filter((d) => d.fileName === sf.fileName && d.category === ts.DiagnosticCategory.Error); const decls = new Map(); -for (const stmt of sf.statements) { +for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; - const decl = stmt.declarationList.declarations[0]!; + const decl = stmt.declarationList.declarations.nodes[0]!; if (ts.isIdentifier(decl.name)) decls.set(decl.name.text, decl); }