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/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 9635ee1f7..6fe670e1e 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 | | `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index b95791a1d..76d27fbc2 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 6a0fb53ef..97e048fc4 100644 --- a/docs/content/docs/experimental.mdx +++ b/docs/content/docs/experimental.mdx @@ -238,8 +238,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/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/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx index db5278c53..a2c324bc3 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, and locale formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `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/builtin-apis/iterator-helpers.mdx b/docs/content/limitations/builtin-apis/iterator-helpers.mdx index 01f779cb6..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, 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. `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/docs/content/limitations/builtin-apis/test-runner.mdx b/docs/content/limitations/builtin-apis/test-runner.mdx index 6583a2873..25eaebfab 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -3,3 +3,7 @@ - **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. +- **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/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/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/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 1bf13ad1c..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": "ef4244aca5b727e3683c", - "rowCount": 3880 -} diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 22a257728..de189f9fd 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/docs/test/fixtures/migration-baseline.json b/docs/test/fixtures/migration-baseline.json index dd4320e03..bd9749038 100644 --- a/docs/test/fixtures/migration-baseline.json +++ b/docs/test/fixtures/migration-baseline.json @@ -300,6 +300,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/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/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 97dff333f..b9657ac21 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": 1175, - "verify-gap": 2153, + "audit-partial": 1176, + "verify-gap": 2152, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -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" ] } } @@ -68887,12 +68894,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 d04995caa..333926a36 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 @@ -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" @@ -91065,6 +91074,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", @@ -94813,8 +94824,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/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})`, ); diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index b2371cb12..3d81a37f7 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"], @@ -372,6 +372,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"] }, @@ -401,7 +402,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/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index adc1816f7..9e85c66dc 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3510,8 +3510,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; @@ -3541,23 +3543,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; @@ -3632,6 +3655,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. */ @@ -3649,6 +3675,7 @@ declare module "node:test" { afterEach, mock, assert, + getTestContext, run, snapshot, TestContext, 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/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 11aad2e57..2651876fc 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/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/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..f5a882bbf 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 { @@ -51,6 +52,7 @@ import { retainBody, traceAdapter, type ShapeHost, + youngEntryLines, } from "./shapes.js"; /** One virtual method slot of a hierarchy: the ROOT-MOST declaring class @@ -75,6 +77,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 +122,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 +335,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 +373,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 +531,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 +617,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 +688,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)); }; @@ -803,6 +852,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); @@ -841,6 +895,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); } @@ -878,14 +933,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[] = []; @@ -944,8 +1017,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/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/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/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index ede7a476e..9e2d3524e 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, @@ -10,6 +11,7 @@ import { RECORD_SOURCES_GATE, WEAK_KEYS_GATE, } 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"; @@ -55,6 +57,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"; @@ -914,6 +917,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; @@ -965,6 +970,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(); @@ -1451,6 +1464,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(), @@ -2507,6 +2521,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`, @@ -4632,6 +4647,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)`); @@ -4664,6 +4682,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)})`, @@ -4941,8 +4965,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); @@ -4956,23 +4980,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(); @@ -4980,10 +5050,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); } } } @@ -5008,7 +5078,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 } } } @@ -5018,7 +5088,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]!); } @@ -5091,7 +5161,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; @@ -5621,7 +5691,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; @@ -5945,7 +6015,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 @@ -6056,7 +6126,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); } @@ -6187,11 +6257,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); @@ -6199,7 +6269,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); } @@ -6365,7 +6435,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); @@ -6511,7 +6581,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, ); } @@ -7536,14 +7606,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); @@ -7704,7 +7774,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}`); @@ -7738,7 +7808,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) { @@ -8083,7 +8154,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, @@ -8130,7 +8201,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; @@ -8157,7 +8228,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 { @@ -8215,7 +8286,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)) { @@ -8238,10 +8310,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; @@ -8268,7 +8340,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); @@ -8292,14 +8364,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); @@ -8350,7 +8424,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, @@ -8415,7 +8489,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); @@ -8565,7 +8639,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 @@ -8631,12 +8705,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); } @@ -8666,12 +8740,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); } @@ -8682,7 +8756,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); @@ -9325,7 +9399,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 @@ -9394,7 +9469,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; @@ -9440,7 +9515,7 @@ export class LlEmitter { s.kind !== "rethrow" && s.kind !== "runtimeFence" ) { - this.releaseFrame(frame); + this.releaseTemps(frame); } } @@ -9652,7 +9727,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); @@ -9693,7 +9768,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; } @@ -9705,11 +9780,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; } @@ -9863,12 +9938,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( @@ -9883,11 +9959,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}`); } @@ -9950,7 +10030,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) { @@ -10179,9 +10259,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 @@ -10384,7 +10475,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 }); } @@ -10637,7 +10728,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); @@ -10733,7 +10824,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 }; } @@ -10772,7 +10863,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, @@ -10795,7 +10886,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 @@ -10820,7 +10911,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. @@ -10836,7 +10927,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..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"; @@ -165,7 +166,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 +225,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 +240,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 +250,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 +719,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 +881,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 +892,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 +979,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 +987,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 +1035,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 @@ -1043,20 +1044,21 @@ 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`, ); 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`); 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 : ""})`); @@ -1121,7 +1123,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 +1136,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 +1190,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 +1241,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 +1249,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 +1258,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 +1266,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 +1394,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 +1410,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 +1456,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 3318ae774..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; @@ -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", @@ -476,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); @@ -490,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/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/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 d7d39f746..3a0e007db 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)`); @@ -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) @@ -716,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 546c9a209..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,17 +373,19 @@ 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 = - 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( ", ", @@ -464,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"; } @@ -486,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( @@ -507,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" @@ -538,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})`); @@ -586,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)`); @@ -741,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)`, ); @@ -765,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)`, @@ -864,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)`, @@ -878,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-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d62c4ca47..baf5eb012 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -316,6 +316,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", @@ -325,6 +326,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. @@ -825,6 +829,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", @@ -892,6 +897,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", @@ -1412,6 +1418,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/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..0b097a1e4 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", [ @@ -49,22 +49,26 @@ 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.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/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..63e4993e8 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", @@ -469,11 +470,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 b789c644d..875f554eb 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/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 951ed61c9..21f4fcb34 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -29,6 +29,7 @@ import { mangleRecordTrace, } from "../mangle.js"; import { + CYC_HDR, cycTypeDescriptor, emitObjectAlloc, emitObjectFree, @@ -58,67 +59,132 @@ import { import { recordMayKeepSource } from "./record-sources.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 { - 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; + /** 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 @@ -1622,6 +1688,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/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 0d65d9456..c7a58a7ea 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -47,6 +47,10 @@ export type StackObjectFieldOwner = (fields: readonly StackObjectField[]) => voi * (stackObjectFields). Published shapes keep heap storage: their field * guards treat the immortal header as a frozen object. */ 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; if (e.type.kind === "record" && host.publishedTypes?.recordGuarded(e.type.shapeId) === true) return false; // A producer with a result-slot entry builds its fresh result in the @@ -232,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.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..85656878a --- /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.retainFrame(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.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.retainFrame(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.retainHeap(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/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index f07bbfa78..b72dc545d 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/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index f11ad6992..756ec7ccf 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 e0ac98afe..e7ba3285c 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 c495b6123..a99ee47cd 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 @@ -644,6 +645,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/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/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/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/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index 66ff502fe..7f64f6614 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; @@ -112,6 +113,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. */ @@ -202,7 +205,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"), ); } @@ -223,6 +227,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" @@ -262,6 +267,7 @@ function routeKey(options: Omit= 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/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/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 a8dea8001..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. @@ -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, @@ -182,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( @@ -202,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 @@ -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/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/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/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 fa880bdc9..32fe09ab3 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(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters 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; @@ -47,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 }; } @@ -56,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]]]]]]): @@ -69,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) @@ -91,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); @@ -113,10 +119,17 @@ 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 }; } + 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, @@ -134,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" @@ -145,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 754eb6aa9..05028e1df 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 { @@ -37,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), @@ -70,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) { @@ -116,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 }; @@ -133,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, @@ -162,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) @@ -186,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 = { @@ -222,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", @@ -253,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", @@ -291,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. @@ -309,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; @@ -336,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", ); } @@ -344,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", @@ -365,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, @@ -385,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) { @@ -402,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; @@ -440,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; @@ -473,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, @@ -483,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) { @@ -513,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); @@ -558,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, }; @@ -584,25 +590,33 @@ 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 }; } +/** 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( @@ -611,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") { @@ -625,13 +640,13 @@ 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) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, @@ -665,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() && @@ -686,13 +702,13 @@ 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) : { kind: "recordGet", - obj: lowerer.lowerExpr(options), + obj: encodingOptions(lowerer, options, optionsType), shapeId: optionsType.shapeId, field: "encoding", type: STRING, @@ -720,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( @@ -737,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") @@ -766,7 +782,7 @@ export function lowerFsStreamCreate( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments, + expr.arguments.nodes, member === "createReadStream", `fs.${member}`, expr, @@ -786,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( @@ -796,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 @@ -824,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( @@ -834,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) { @@ -857,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); @@ -907,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 ( @@ -921,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", @@ -954,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"; @@ -965,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[] = [ @@ -1012,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) }]); } @@ -1020,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; @@ -1057,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, @@ -1080,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", @@ -1116,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 013b16e7c..b89e25f39 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -19,6 +19,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 @@ -35,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`, @@ -46,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 }; } @@ -55,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) && @@ -67,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 @@ -102,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") @@ -152,9 +153,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), @@ -191,9 +201,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" || toJsonClass) && !jsonResultObserved(lowerer, call) ? { kind: "toString", operand: raw, type: STRING, loc } : { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc }; } @@ -290,6 +302,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; +} + /** True when a class field type holds a RegExp or a Promise outside a * nested class instance (whose own fields are checked separately): Node * writes both as {}, and the runtime JSON traversal has no writer for them. */ @@ -461,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 614e8c873..54ab22c05 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -43,13 +43,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; @@ -74,7 +77,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; @@ -90,9 +93,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, @@ -114,7 +117,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 }; } @@ -138,8 +141,9 @@ function lowerFromCharCodeApply( ) { return null; } - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) return null; - const listNode = call.arguments[1]!; + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; + const listNode = call.arguments.nodes[1]!; const checkerT = lowerer.typeOf(listNode); const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT); @@ -150,7 +154,7 @@ function lowerFromCharCodeApply( return null; } const loc = locOf(call); - const thisArg = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + const thisArg = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN); const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }]; if (listT.kind !== "dyn") { const list = lowerer.lowerExprExpecting(listNode, listT); @@ -225,7 +229,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, @@ -233,12 +237,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/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 8ff9d0a21..2a9ff9eef 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -48,12 +48,12 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (callee.questionDotToken || call.questionDotToken) { lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT); } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } // JsonWebKey imports are not implemented: a literal 'jwk' format is // refused here (the runtime refuses a computed one). - const format = call.arguments[0]; + const format = call.arguments.nodes[0]; if ( member === "importKey" && format !== undefined && @@ -63,7 +63,7 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); } const loc = locOf(call); - const args = call.arguments.map((arg) => + const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) ? lowerDynObjectLiteral(lowerer, arg) : lowerer.lowerExprExpecting(arg, DYN), 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 d98f80feb..9d9c9668c 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -13,7 +13,13 @@ import { typeEquals, } from "../../ir/ir.js"; import { isJsSourceFile, isNodeEsmFile } from "../program.js"; -import { genResultRecord, isAnyArrayType, jsOpenObjectType } from "../type-mapper.js"; +import { + genResultRecord, + isAnyArrayType, + isUnitOnlyTsType, + jsOpenObjectType, + unitOnlyUnion, +} from "../type-mapper.js"; import { dynFallbackType } from "./lowerer.js"; import { builtinModuleFnOf } from "./surfaces.js"; import { mixinFnShapeOf } from "./lower-mixins.js"; @@ -183,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 && @@ -380,7 +386,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, @@ -389,6 +395,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) { @@ -623,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). @@ -658,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); @@ -717,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)); @@ -754,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 && @@ -764,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 @@ -786,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) @@ -806,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; } @@ -843,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-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/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 7c8770021..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 || @@ -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/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/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/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 c41bf5163..7f1f758ca 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). @@ -49,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, @@ -76,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 }; @@ -108,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. @@ -188,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", @@ -215,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 }; } @@ -276,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", @@ -292,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); @@ -355,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), @@ -413,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)); @@ -437,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())) { @@ -450,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 }, ], @@ -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); @@ -476,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 { @@ -484,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 = { @@ -501,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", @@ -520,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, @@ -547,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 }; @@ -576,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, @@ -606,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, @@ -644,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) { @@ -682,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, @@ -725,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 }; @@ -740,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", @@ -769,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 } @@ -798,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", @@ -817,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", @@ -838,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 @@ -852,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", @@ -874,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; @@ -968,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 => @@ -1025,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", @@ -1105,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); @@ -1155,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", @@ -1176,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); @@ -1283,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)); @@ -1312,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 && @@ -1355,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 }; } @@ -1505,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 7cb3753ac..aeefffe48 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,19 @@ 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, + lowerCollectionIteratorValue, + lowerStringIteratorValue, +} from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; /* ── iterator helpers (ES2025) ─────────────────────────────────────────── */ @@ -73,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; @@ -83,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 @@ -146,19 +154,49 @@ 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. + 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 @@ -167,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 @@ -189,6 +231,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 +261,125 @@ 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.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); + 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, 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( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + 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)); + 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); + iterator = lowerCollectionIteratorValue( + lowerer, + value, + t.kind === "set" ? "values" : "entries", + own(elem), + loc, + ); + } + 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 @@ -234,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; @@ -353,14 +536,40 @@ 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.nodes.length !== 1 + ) + return null; + 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; +} + +/** `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) + 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; @@ -401,8 +610,23 @@ 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) + 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; @@ -464,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) ); @@ -480,6 +705,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]; @@ -505,6 +735,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 }; @@ -577,10 +815,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", ); } @@ -593,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 ( @@ -603,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 ( @@ -625,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; @@ -648,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, @@ -656,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 }; } @@ -749,7 +994,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, @@ -773,21 +1019,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 }; @@ -1079,6 +1327,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( @@ -1245,8 +1594,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 +1607,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[] = @@ -1768,7 +2139,7 @@ function buildIterChainFn( function buildHelperGenFn( lowerer: Lowerer, name: string, - srcT: GenType, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, argT: IrType, @@ -1777,8 +2148,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: { @@ -1804,12 +2179,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); @@ -1824,12 +2194,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); @@ -1860,28 +2225,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) => [ { @@ -1905,12 +2254,7 @@ function buildHelperGenFn( ); } else { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const innerT = fnT!.ret; @@ -1932,7 +2276,14 @@ 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, + tag: "Iterator Helper", + // 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/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts new file mode 100644 index 000000000..f936e27cb --- /dev/null +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -0,0 +1,302 @@ +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.nodes[0] === outerValue(drain) && + ts.isPropertyAccessExpression(parent.expression) && + parent.expression.name.text === "from" && + lowerer.isStdlibGlobal(parent.expression.expression, "Array") + ) { + const mapper = parent.arguments.nodes[1]; + if (mapper === undefined) return rowsReadByElements(lowerer, parent, 0); + return parent.arguments.nodes.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.nodes.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.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.nodes.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.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; + 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.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)) + 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?.nodes.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.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"; + 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..832dc7def 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 { @@ -737,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 { @@ -750,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" }]), @@ -818,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(); @@ -862,14 +864,14 @@ 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 { kind: "regexIntrinsic", - method: "matchAll", + method: matchAllRowsReadByElements(lowerer, call, false) ? "matchAllCaptures" : "matchAll", receiver, args: [re], type: arrayOf(arrayOf(STRING)), @@ -881,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 }; @@ -896,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, @@ -912,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]; @@ -945,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", @@ -982,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) || @@ -1045,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") @@ -1368,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 48a5712e3..13f3f2dbd 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -1,5 +1,11 @@ import * as ts from "../ts7/adapter.js"; -import type { IrExpr, IrFunction } 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 type { ClassInfo } from "./lower-classes.js"; import type { Lowerer } from "./lowerer.js"; @@ -56,6 +62,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 +83,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 +112,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 +127,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 +135,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 +161,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 @@ -224,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 c95731fc7..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 @@ -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", @@ -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,10 +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) && - 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) => @@ -1276,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 )[]; @@ -1333,14 +1336,14 @@ 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); + const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { lowerer.unsupported( "SC1090", @@ -1462,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", @@ -1480,9 +1483,14 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && shape.fields.length > 0 && - expr.properties.length > 0 && - expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) && - expr.properties.some((p) => { + expr.properties.nodes.length > 0 && + expr.properties.nodes.every( + (p) => + ts.isSpreadAssignment(p) && + !conditionalSpreadOf(p.expression) && + !andSpreadOf(lowerer, p.expression), + ) && + 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) @@ -1509,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); @@ -1588,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. @@ -1599,7 +1607,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 @@ -1666,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), ); @@ -1726,9 +1734,10 @@ 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)) 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. @@ -1961,9 +1970,10 @@ 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)) 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. @@ -2206,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) @@ -2339,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)) @@ -2353,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)) @@ -2517,6 +2527,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.nodes.length === 0) return "unsupported"; + const props: CondSpreadProp[] = []; + for (const p of right.properties.nodes) { + 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) & { @@ -2539,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 && @@ -2587,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 ceedc3e94..930e1a7cb 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 @@ -135,6 +137,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, @@ -181,7 +208,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)) { @@ -208,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); @@ -384,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 5ba63be26..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" ) { @@ -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 => { @@ -233,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"; }) @@ -271,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; @@ -296,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 49bb33882..bf5f2563d 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, storedClassValueType, type ClassInfo } from "./lower-classes.js"; @@ -206,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"); @@ -244,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 }; } @@ -278,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 @@ -287,18 +288,26 @@ 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); if (local) return local; - 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. An async + // body's promise is always present and settles with undefined there. + const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) + ? 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 // LITERAL is semantic — the instance body reads the named field — so @@ -429,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; } @@ -547,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 ( @@ -577,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); @@ -588,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; } @@ -838,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") { @@ -900,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; @@ -1044,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. @@ -1067,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; @@ -1129,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) { @@ -1291,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; @@ -1332,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); @@ -1398,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; @@ -1447,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", @@ -1455,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 && @@ -1498,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", @@ -1574,6 +1589,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; @@ -1642,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) || @@ -1707,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. @@ -1732,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") { @@ -1872,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()); } @@ -1916,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 && @@ -1957,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); @@ -2160,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 @@ -2179,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, @@ -2293,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 @@ -2309,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/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index fb59c4cee..898c46f20 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -258,13 +258,350 @@ 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, }); } 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, + tag: "!", + }, + 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 + * `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, tag: "Array Iterator" }, + 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, tag: "String Iterator" }, + 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 @@ -419,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, }); } @@ -690,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/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 b027cd8d1..4845fc1a1 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 @@ -64,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"; @@ -93,6 +95,8 @@ import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/ import { lowerGroupByStaticCall } from "./containers/collection-grouping.js"; import { lowerGeneratorHelperCall, + lowerHelperReturnCall, + lowerIteratorFromCall, lowerIteratorHelperCall, } from "./containers/iterator-helpers.js"; import { @@ -157,6 +161,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, @@ -221,6 +226,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 { @@ -242,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; } @@ -363,6 +369,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); @@ -528,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 { @@ -539,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 }; @@ -579,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); @@ -598,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 && @@ -617,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 ? [] @@ -647,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)); @@ -721,10 +730,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); @@ -747,10 +756,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 }; @@ -817,10 +826,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 { @@ -854,7 +863,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 @@ -902,9 +911,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); @@ -935,10 +944,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")) { @@ -975,7 +984,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; @@ -998,14 +1007,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); @@ -1040,13 +1049,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") { @@ -1073,20 +1082,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`, @@ -1106,8 +1115,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; @@ -1136,7 +1145,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). @@ -1148,9 +1158,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 @@ -1159,7 +1172,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, { @@ -1252,7 +1265,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, @@ -1315,7 +1328,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, @@ -1474,6 +1487,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) ?? @@ -1532,13 +1547,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 +1566,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, @@ -1623,6 +1641,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.nodes.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)) { @@ -1669,7 +1698,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") @@ -1682,7 +1711,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, }; @@ -1728,7 +1757,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, @@ -1783,7 +1817,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. @@ -1791,7 +1825,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) @@ -1810,7 +1844,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, }; @@ -1831,7 +1865,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; } @@ -1843,17 +1877,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 }; @@ -1876,14 +1910,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"); @@ -1892,7 +1927,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, }; @@ -1907,18 +1942,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]!)], }; } @@ -1941,7 +1977,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 || !( @@ -1953,7 +1989,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", ); } @@ -1965,7 +2001,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", ); } @@ -2038,7 +2074,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; @@ -2145,6 +2181,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( @@ -2261,7 +2300,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 = @@ -2332,7 +2371,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 } }; @@ -2343,7 +2382,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) }; @@ -2374,7 +2413,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 = @@ -2441,7 +2480,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; @@ -2456,10 +2495,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", @@ -2488,15 +2527,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, ); } @@ -2511,7 +2550,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)) { @@ -2522,8 +2561,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, ); @@ -2777,17 +2816,18 @@ 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); - 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, @@ -2802,12 +2842,18 @@ 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 = 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 }; } @@ -2842,7 +2888,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; @@ -2868,7 +2914,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 { @@ -2897,7 +2943,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") @@ -2920,12 +2966,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); @@ -2939,7 +2985,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 @@ -2974,7 +3023,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"); @@ -2985,7 +3034,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) { @@ -3001,7 +3050,7 @@ function lowerStringPrototypeCall( indexMethod, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); if (!entry) return null; if (padding) { @@ -3011,7 +3060,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (entry.method === "split") { @@ -3020,7 +3069,7 @@ function lowerStringPrototypeCall( call, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } const receiver = lowerer.ensureString(receiverValue, receiverNode); @@ -3029,7 +3078,7 @@ function lowerStringPrototypeCall( call, methodAccess, () => receiver, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } @@ -3161,10 +3210,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", @@ -3224,25 +3295,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); @@ -3290,7 +3353,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 { @@ -3301,7 +3364,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, }, @@ -3372,11 +3435,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); @@ -3440,8 +3503,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, @@ -3490,8 +3553,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", @@ -3507,7 +3570,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", @@ -3623,13 +3686,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, @@ -3931,7 +3994,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); @@ -3961,11 +4024,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)", ); } @@ -3977,15 +4042,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 = @@ -3995,7 +4060,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 && @@ -4021,7 +4086,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 { @@ -4035,7 +4100,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), }, @@ -4045,7 +4110,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 @@ -4101,8 +4166,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") { @@ -4190,12 +4255,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", @@ -4237,8 +4302,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; @@ -4254,8 +4319,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) }, ], @@ -4263,9 +4328,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") { @@ -4314,7 +4379,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; @@ -4327,8 +4392,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) }; } @@ -4347,7 +4419,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; @@ -4416,7 +4488,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); } @@ -4432,9 +4504,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); @@ -4442,7 +4514,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); @@ -4522,8 +4594,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); @@ -4536,14 +4608,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; @@ -4552,16 +4624,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 @@ -4600,7 +4672,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, @@ -4608,7 +4680,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, @@ -4616,7 +4688,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) || @@ -4638,16 +4710,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") { @@ -4695,7 +4767,7 @@ function implicitReceiverOverloadReturn( if ( !ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.some(ts.isSpreadElement) ) { return null; } @@ -4712,12 +4784,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]!)); @@ -4749,7 +4821,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, ); } @@ -4863,7 +4935,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( @@ -4882,7 +4954,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)) { @@ -5125,7 +5197,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); } @@ -5168,15 +5240,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" @@ -5251,9 +5323,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)); @@ -5500,8 +5572,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 @@ -5543,7 +5616,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)`, ); }; @@ -5581,7 +5654,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, @@ -5618,7 +5691,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)", ); } @@ -5630,7 +5703,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)}')`, ); } @@ -5726,7 +5799,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", @@ -5742,7 +5815,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 () => { ... })", ); } @@ -5836,7 +5909,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( @@ -5887,7 +5960,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", @@ -5936,23 +6009,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); } @@ -6010,7 +6083,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 { @@ -6102,9 +6175,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 = @@ -6337,21 +6410,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", ); } @@ -6379,12 +6452,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", @@ -6405,11 +6478,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 }; } @@ -6430,12 +6503,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; @@ -6641,10 +6714,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, @@ -6652,11 +6725,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()", ); } @@ -6674,12 +6751,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) }; } @@ -6715,7 +6792,7 @@ function lowerIntlNumberFormatCall( ) { return null; } - ctorArgs = recv.arguments ?? []; + ctorArgs = recv.arguments?.nodes ?? []; } else { return null; } @@ -6747,13 +6824,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))}'`, @@ -6802,10 +6879,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" || @@ -6829,12 +6906,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, @@ -6851,7 +6928,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") { @@ -6873,7 +6950,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", @@ -6884,8 +6961,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 && @@ -6925,16 +7002,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; @@ -7060,18 +7140,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 @@ -7104,9 +7184,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", @@ -7125,10 +7205,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)) || @@ -7142,7 +7222,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, }, @@ -7159,14 +7241,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); @@ -7186,17 +7271,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 }; @@ -7220,8 +7308,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, @@ -7253,15 +7341,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); @@ -7281,7 +7371,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++) { @@ -7350,12 +7440,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 = @@ -7379,7 +7469,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" && @@ -7390,12 +7480,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 } : {}), }; } @@ -7406,8 +7496,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 { @@ -7418,7 +7508,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", @@ -7437,12 +7527,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; @@ -7460,20 +7550,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", @@ -7488,10 +7578,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" || @@ -7535,15 +7625,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; @@ -7598,11 +7688,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") { @@ -7660,8 +7750,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 @@ -8242,11 +8333,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; @@ -8257,7 +8349,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)); @@ -8403,7 +8495,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") @@ -8417,7 +8509,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, }; @@ -8488,7 +8580,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", @@ -8531,8 +8623,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; @@ -8551,8 +8643,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}`; @@ -8645,7 +8737,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, @@ -8666,8 +8758,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; @@ -8685,14 +8777,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 => { @@ -8703,7 +8795,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", @@ -8743,7 +8835,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 }; @@ -8837,7 +8929,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 { @@ -8845,7 +8937,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, }; @@ -8873,7 +8965,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); @@ -8882,7 +8974,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, }; @@ -8906,7 +8998,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 }; } @@ -8923,6 +9015,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. @@ -9117,14 +9217,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, @@ -9139,10 +9239,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, @@ -9174,7 +9274,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") @@ -9187,7 +9287,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, }; @@ -9208,7 +9308,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}`); @@ -9232,7 +9332,7 @@ export function lowerObjectMethodCall( lowerer, value, method, - call.arguments, + call.arguments.nodes, fallback, loc, access.getText(), @@ -9264,7 +9364,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", @@ -9325,7 +9425,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") @@ -9338,7 +9438,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, }; @@ -9369,7 +9469,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, @@ -9419,7 +9519,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); @@ -9491,7 +9591,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 80fde36bd..770c74840 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, @@ -158,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 => { @@ -298,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); @@ -367,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) || @@ -517,6 +521,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 @@ -536,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; @@ -752,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[] = []; @@ -779,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 @@ -857,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: [], @@ -889,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: [], @@ -920,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: [], @@ -1054,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) || @@ -1081,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) @@ -1390,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 @@ -1450,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; @@ -1493,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 = @@ -1525,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 @@ -1542,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); @@ -1559,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; @@ -1599,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 && @@ -1621,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( @@ -1635,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 @@ -1707,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 @@ -1773,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"; @@ -1874,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 @@ -1915,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; } @@ -1945,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; } @@ -1963,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 && @@ -2004,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; @@ -2015,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; @@ -2128,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; @@ -2141,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"); @@ -2218,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) && @@ -2263,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) && @@ -2285,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()) && @@ -2453,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 @@ -2548,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; @@ -2658,9 +2699,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", @@ -2673,9 +2720,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 @@ -2687,7 +2735,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())) { @@ -2695,7 +2743,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)) { @@ -2788,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) || @@ -2946,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 || @@ -2958,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 @@ -3238,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 && @@ -3302,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)); @@ -3345,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 @@ -3455,12 +3520,22 @@ 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 { // 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; @@ -3532,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. @@ -3561,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 && @@ -3593,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) && @@ -3602,6 +3677,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) { @@ -3639,11 +3724,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; @@ -3755,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) && @@ -3993,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)", ); } @@ -4005,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 && @@ -4029,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, ), @@ -4119,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); @@ -4536,8 +4636,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 { @@ -4573,8 +4672,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 @@ -4875,6 +4973,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.nodes) { + 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( @@ -4947,11 +5097,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) || @@ -4978,14 +5137,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)) @@ -5190,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()) && @@ -5205,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( @@ -5221,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) || @@ -5255,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; } } @@ -5292,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) && @@ -5322,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); @@ -5620,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 ( @@ -5763,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, }; @@ -5810,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)) { @@ -5827,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) { @@ -5841,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) { @@ -5879,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 }; } @@ -5931,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(); @@ -5969,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), }; @@ -6120,6 +6360,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) || @@ -6219,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), }; } @@ -6358,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(); @@ -6391,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) @@ -6455,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()) ) { @@ -6485,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) && @@ -6500,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(); @@ -6527,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) { @@ -6603,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), ); } @@ -6735,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, @@ -6767,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; @@ -6936,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); @@ -6951,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)); } @@ -7004,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) && @@ -7448,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. @@ -7458,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}`, @@ -7493,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)); @@ -7520,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}`, @@ -7650,8 +7902,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 { @@ -7666,8 +7917,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 @@ -7773,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, @@ -7781,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, @@ -7800,7 +8050,7 @@ export function lowerDerivedCtorBody( lowerer, info, base, - superCall.arguments, + superCall.arguments.nodes, thisLocal, loc, superCall, @@ -7812,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, @@ -7827,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, @@ -7851,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), @@ -7895,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: { @@ -8097,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, @@ -8160,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, }; @@ -8177,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}`, @@ -8440,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 { @@ -8545,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) && @@ -8592,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]))) { @@ -8608,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)) && @@ -8659,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')", ); } @@ -8684,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 @@ -8770,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, @@ -8811,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 @@ -8826,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); } @@ -8865,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); } @@ -8878,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); } @@ -8893,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); } @@ -8907,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", @@ -8921,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')", ); } @@ -8945,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). @@ -8955,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); } @@ -9035,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`, @@ -9089,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); } @@ -9162,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); @@ -9194,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 @@ -9214,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, @@ -9225,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 }; } @@ -9244,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) ) { @@ -9282,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", @@ -9306,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, @@ -9336,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 && @@ -9353,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, @@ -9377,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, @@ -9390,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"); } @@ -9462,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" || @@ -9479,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", @@ -9543,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", @@ -9667,24 +9924,56 @@ 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?.nodes.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?.nodes ?? [], + 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); @@ -9750,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 7e3a70473..adde0eb3f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -20,6 +20,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"; @@ -39,6 +40,8 @@ import { lowerZlibStreamBytesWritten, } from "./builtins/compression.js"; import { + runtimeClassValueOf, + runtimeClassValueOfType, builtinClassAliasOwnsNoSlot, builtinClassRefOf, sameBuiltinClass, @@ -147,6 +150,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, @@ -221,6 +225,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, @@ -551,7 +560,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), @@ -954,6 +963,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)) { @@ -1267,6 +1283,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); @@ -1796,6 +1818,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 @@ -1901,6 +1935,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); @@ -1947,12 +1985,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const table = lowerInternTableProperty(lowerer, expr); if (table) return table; } + lowerThreadSlotsProperty(lowerer, expr); // `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin // class alias. if (expr.name.text === "name" && !expr.questionDotToken) { 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); @@ -2156,13 +2205,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); } } @@ -2204,7 +2251,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 && @@ -2213,7 +2260,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, }; @@ -2972,11 +3019,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) : []; @@ -3050,7 +3097,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 @@ -4035,7 +4082,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; @@ -4089,7 +4136,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) @@ -5833,11 +5880,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) && @@ -5933,7 +5980,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]!; @@ -5945,11 +5992,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 && @@ -5968,7 +6015,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]!; } @@ -5980,7 +6027,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 @@ -5996,12 +6043,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 && @@ -6025,7 +6072,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", @@ -6039,7 +6086,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), ) ) @@ -6049,7 +6096,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) ) { @@ -6062,7 +6109,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; @@ -6074,11 +6121,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 @@ -6086,7 +6133,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) }; @@ -6110,14 +6157,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", @@ -6145,7 +6192,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; @@ -6167,8 +6214,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) { @@ -6176,7 +6223,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, @@ -6421,7 +6468,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)); @@ -7324,25 +7371,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); @@ -7354,7 +7414,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`. */ @@ -7377,14 +7437,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: { @@ -7414,8 +7476,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); @@ -7479,6 +7542,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 @@ -7518,7 +7664,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, () => @@ -7534,7 +7688,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, @@ -8124,7 +8278,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, }; @@ -8142,7 +8296,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, }; @@ -8525,6 +8679,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 { @@ -8893,10 +9049,24 @@ 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") 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", @@ -8936,6 +9106,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 @@ -8994,6 +9167,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, @@ -9127,7 +9302,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]!); @@ -9150,7 +9325,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) ?? @@ -9196,7 +9371,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", @@ -9467,6 +9642,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; @@ -9935,7 +10112,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, }, @@ -10998,8 +11175,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, }; @@ -12360,7 +12537,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; @@ -12447,7 +12624,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, ); @@ -12882,9 +13059,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", @@ -12902,6 +13092,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 @@ -13200,6 +13399,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. */ @@ -13253,7 +13583,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; }); @@ -14108,11 +14438,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); @@ -14186,7 +14516,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; } @@ -14212,17 +14542,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 @@ -14289,7 +14619,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)) { @@ -14303,17 +14633,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; } @@ -14342,15 +14672,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 ( @@ -15515,10 +15845,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)) @@ -16053,7 +16383,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, @@ -16066,7 +16396,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 @@ -16131,18 +16461,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))) { @@ -16151,7 +16481,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" @@ -16329,7 +16659,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/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index c7e293707..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)); @@ -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) @@ -301,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, @@ -321,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 @@ -492,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, @@ -504,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"), ); @@ -516,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 1d67cef92..be7f21c1c 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[] => [ @@ -253,6 +257,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 +351,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 { @@ -362,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", @@ -463,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: @@ -494,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") && @@ -1184,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; @@ -1304,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!(); @@ -1540,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`, @@ -1721,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 @@ -1979,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, @@ -2326,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 { @@ -2358,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, @@ -2372,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 })), ], @@ -2413,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", @@ -2458,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" @@ -2472,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`, @@ -2491,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 }; @@ -2533,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 { @@ -2616,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 74924e775..4ca681c2a 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -19,6 +19,7 @@ import { canonicalBuiltinModule, cjsExportAssignmentOf, cjsExportChainOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, cjsExportTargetLiteral, entryPackageFilePredicate, @@ -243,7 +244,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 +294,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 +368,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 +386,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 +441,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 +516,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 +526,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 +547,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 +634,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 +649,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 +685,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 +1072,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 +1166,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 +1288,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; @@ -1421,6 +1422,11 @@ export function collectGlobals( for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value); 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; } @@ -1465,7 +1471,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) { @@ -1496,7 +1502,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)) { @@ -1574,8 +1580,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; } @@ -1590,14 +1596,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); } } @@ -1609,7 +1615,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); @@ -1641,7 +1647,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; @@ -1656,8 +1663,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 @@ -2006,7 +2013,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) || @@ -2053,7 +2060,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) && @@ -2177,7 +2184,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 @@ -2270,7 +2277,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; } @@ -2344,7 +2351,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", ) ) { @@ -2597,7 +2604,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 { @@ -2703,7 +2710,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) }; @@ -2877,7 +2884,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/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 20caf184b..92d9f0690 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, @@ -213,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, @@ -237,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; } @@ -342,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 { @@ -463,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; @@ -568,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) { @@ -607,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()", @@ -659,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; } @@ -1131,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 }]; @@ -1150,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 }]; }), @@ -1194,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; } @@ -1232,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), @@ -1271,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 @@ -1408,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", @@ -1417,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", @@ -1471,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", @@ -1489,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", @@ -1498,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", @@ -1512,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", @@ -1536,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", @@ -1547,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 @@ -1575,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]", ); } @@ -1606,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" @@ -1626,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( @@ -1659,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 ) { @@ -1681,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, @@ -1712,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" @@ -1728,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, @@ -1760,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 ) { @@ -1785,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( @@ -1813,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); @@ -1848,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); @@ -1883,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); @@ -1906,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); @@ -1930,13 +1935,42 @@ 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; - 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.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length === 0) + return null; + return lowerResponseConstruction(lowerer, call.arguments.nodes, 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 +2093,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, }; @@ -2078,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 ) { @@ -2120,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", @@ -2129,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}`); @@ -2322,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); } @@ -2342,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, @@ -2355,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 @@ -2366,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 }; } } @@ -2388,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. @@ -2417,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; @@ -2427,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 7a4214ede..69a14773d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,4 +1,9 @@ +import { + matchAllRowsReadByElements, + storedMatchAllRowsReadByElements, +} from "./containers/match-row-uses.js"; 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"; @@ -74,6 +79,7 @@ import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, cjsExportChainOf, + cjsVoidInitPreambleReals, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -236,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" @@ -562,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"); @@ -590,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; @@ -687,10 +693,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 { @@ -755,10 +758,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 @@ -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); @@ -1012,12 +1013,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); } @@ -1247,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); @@ -1275,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(); @@ -1348,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); } @@ -1592,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). @@ -1754,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; @@ -1861,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); @@ -1878,7 +1890,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, @@ -2119,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"; @@ -2196,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"); @@ -2218,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; @@ -2293,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) { @@ -2378,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, ) ? { @@ -2391,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) { @@ -2462,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) { @@ -2503,6 +2517,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.nodes.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", @@ -2510,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 @@ -2547,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 @@ -2594,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 = @@ -2687,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) { @@ -2696,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 @@ -2804,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) { @@ -2875,7 +2922,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 @@ -3167,7 +3214,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) { @@ -3272,7 +3319,7 @@ function arrayPositionBinding( * numeric position or `length` (`const { 0: a, 1: b, length } = * s.split(".")`), with no rest element. */ function isArrayPositionPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { - return pattern.elements.every((el) => { + return pattern.elements.nodes.every((el) => { if (el.name === undefined) return true; if (el.dotDotDotToken) return false; const prop = el.propertyName ?? el.name; @@ -3299,7 +3346,7 @@ function lowerArrayPositionObjectPattern( isLet: boolean, out: IrStmt[], ): void { - for (const el of pattern.elements) { + for (const el of pattern.elements.nodes) { if (el.name === undefined) continue; const name = patternKeyNameOf(lowerer, (el.propertyName ?? el.name) as ts.PropertyName)!; const value: IrExpr = @@ -3374,7 +3421,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 @@ -3386,7 +3433,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; @@ -3484,7 +3531,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( @@ -3940,7 +3987,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; @@ -4315,7 +4362,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)) { @@ -4329,7 +4376,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. @@ -4527,7 +4574,7 @@ export function lowerVarDecl( !isJsSourceFile(decl.getSourceFile()) && !decl .getSourceFile() - .statements.some( + .statements.nodes.some( (s) => ts.isImportDeclaration(s) || ts.isExportDeclaration(s) || @@ -4844,7 +4891,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) || @@ -5002,7 +5049,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]; @@ -5342,7 +5389,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 = @@ -5453,7 +5500,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)) || @@ -5469,7 +5516,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; @@ -5579,7 +5626,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; @@ -5638,7 +5685,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; } @@ -5686,7 +5733,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); @@ -5702,7 +5749,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, ), }); @@ -5804,7 +5854,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) && @@ -5822,13 +5872,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, ), })); @@ -5919,12 +5969,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))) ); } @@ -6200,50 +6250,10 @@ 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) { + 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); @@ -6334,7 +6344,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 @@ -6934,7 +6944,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 @@ -7395,7 +7407,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; @@ -7578,8 +7590,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) { @@ -7635,7 +7647,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)) { @@ -7657,7 +7669,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", @@ -7675,7 +7687,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 @@ -7705,13 +7717,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); @@ -7848,7 +7860,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( @@ -7923,7 +7935,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). @@ -7943,7 +7955,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); @@ -8289,7 +8301,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({ @@ -8307,13 +8319,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. @@ -8635,7 +8647,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 && @@ -8699,7 +8711,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); @@ -8730,7 +8742,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" && @@ -9243,7 +9255,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 @@ -9424,7 +9436,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); @@ -9561,13 +9573,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; } @@ -9599,7 +9611,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); @@ -9613,7 +9625,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({ @@ -9624,7 +9636,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, @@ -9768,7 +9782,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()); @@ -10000,7 +10014,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 }); @@ -10183,7 +10197,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 { @@ -10549,7 +10563,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; @@ -10662,7 +10676,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 || @@ -10773,7 +10787,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 e36518559..5c6011425 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"; @@ -90,12 +91,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 +112,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 +123,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); @@ -152,7 +174,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) @@ -168,15 +190,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 +324,116 @@ 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.nodes; + 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; + // 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 { + 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 +443,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 +455,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 +525,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 +561,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), @@ -459,7 +590,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`, @@ -522,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", @@ -540,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.nodes.length > 0) { + lowerer.noLowering( + `getTestContext with ${expr.arguments.nodes.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 @@ -596,7 +763,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, @@ -604,7 +771,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, @@ -612,11 +779,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", @@ -683,7 +850,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" || @@ -695,7 +862,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); }); @@ -772,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. @@ -893,7 +1061,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); @@ -1052,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/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 44b2b6a1d..740ff66f1 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, @@ -283,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; @@ -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", @@ -355,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, @@ -363,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 }; @@ -392,15 +412,14 @@ 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; - 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); @@ -410,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, @@ -419,11 +438,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)", + expr.arguments.nodes[0]!, + "the handle is the Timeout setTimeout returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearTimeout", args: [handle], type: VOID, loc }; @@ -431,15 +451,15 @@ 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 = lowerer.lowerExpr(expr.arguments[0]!); + 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]!, - "the handle is the number setInterval returned (narrow `number | null` first)", + expr.arguments.nodes[0]!, + "the handle is the Timeout setInterval returned, or undefined or null", ); } return { kind: "libCall", fn: "timers.clearInterval", args: [handle], type: VOID, loc }; @@ -448,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, @@ -456,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 }; @@ -475,15 +495,15 @@ 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 = lowerer.lowerExpr(expr.arguments[0]!); + 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]!, - "the handle is the Immediate setImmediate returned (narrow 'Immediate | undefined' first)", + expr.arguments.nodes[0]!, + "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/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 b2d302b55..98e83eced 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -5,6 +5,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, @@ -242,6 +243,7 @@ import { isOverloadedCallableMemberType, isUnitOnlyTsType, isWorkerHandleType, + isFetchHandleType, jsOpenObjectType, mapType, ShapeRegistry, @@ -268,7 +270,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, @@ -301,6 +307,8 @@ import { type ClassIteratorInfo, type GenericClassInfo, registerBuiltinErrorClasses, + defineClassValueConstruct, + runtimeClassConstructs, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, @@ -670,7 +678,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; @@ -687,7 +695,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) || @@ -844,7 +852,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; @@ -852,35 +878,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( @@ -1178,7 +1186,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}`; @@ -1495,15 +1503,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) @@ -1539,7 +1547,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 @@ -2005,6 +2013,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(); @@ -2463,6 +2475,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 @@ -2663,6 +2684,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 @@ -2751,6 +2781,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; @@ -2902,6 +2935,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). */ @@ -2919,28 +2978,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(); @@ -2954,6 +3014,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 } @@ -2969,10 +3030,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)) @@ -3011,7 +3072,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; @@ -3043,6 +3104,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 @@ -3087,12 +3152,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) ); } @@ -3161,6 +3228,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}`; @@ -3335,7 +3406,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) @@ -3345,9 +3416,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); @@ -4043,7 +4114,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)) && @@ -4189,7 +4260,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) @@ -4215,9 +4286,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); @@ -4359,7 +4430,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 => { @@ -4560,6 +4631,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?.nodes.length ?? 0) > 0; + for (const member of node.members.nodes) + if ( + ts.isMethodDeclaration(member) && + member.body && + (genericClass || (member.typeParameters?.nodes.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; @@ -4575,14 +4719,18 @@ 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"; }; 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; @@ -4656,7 +4804,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") @@ -4824,7 +4972,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))) @@ -4859,7 +5007,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); @@ -4884,7 +5032,7 @@ export class Lowerer { initSymbol !== undefined && (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); - const positions = name.elements.filter((el) => { + const positions = name.elements.nodes.filter((el) => { const key = el.propertyName; return ( key !== undefined && ts.isNumericLiteral(key) && /^(?:0|[1-9]\d{0,8})$/.test(key.text) @@ -4896,7 +5044,7 @@ export class Lowerer { const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; return scanPositions( - name.elements.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), + name.elements.nodes.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), init, sourceType, ); @@ -4948,7 +5096,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)) @@ -5093,7 +5241,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); @@ -5207,7 +5355,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; @@ -5229,7 +5379,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; @@ -5265,7 +5415,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; @@ -5447,12 +5597,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) && @@ -5678,7 +5828,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; @@ -5774,9 +5924,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); @@ -5839,7 +5989,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; @@ -5873,7 +6023,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 || @@ -5881,10 +6031,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] : [], @@ -5901,10 +6051,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 @@ -5916,7 +6066,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)) { @@ -5936,7 +6086,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; @@ -5956,7 +6106,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); @@ -6005,7 +6155,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); @@ -6030,7 +6180,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, @@ -6115,6 +6265,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 @@ -6225,7 +6390,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); } @@ -6477,8 +6642,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; @@ -6625,6 +6790,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 @@ -6756,6 +6922,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] } : {}), }; @@ -6888,7 +7057,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] : [], + ), ), ), ); @@ -6928,10 +7099,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), ); @@ -6964,7 +7135,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. @@ -7297,6 +7468,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. @@ -8053,7 +8225,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 @@ -8428,6 +8606,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 @@ -8647,6 +8837,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 }; } } @@ -8767,6 +8958,7 @@ export class Lowerer { }); if (baseTag >= 0) { if (!sub || !this.ctorAbiEquals(sub, c)) break; + defineClassValueConstruct(this, c); const widened: IrExpr = { kind: "upcast", value: expr, @@ -9390,13 +9582,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), }; @@ -9442,7 +9634,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 && @@ -9454,20 +9646,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" && @@ -9507,8 +9699,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); @@ -10082,7 +10274,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) || @@ -10090,18 +10282,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; @@ -10123,7 +10315,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 @@ -10374,7 +10566,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 @@ -10602,7 +10794,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", @@ -10882,7 +11074,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 || @@ -11547,6 +11739,7 @@ export class Lowerer { return ( lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? + lowerThreadSlotsCall(this, expr) ?? lowerFfiCall(this, expr) ?? lowerFetchCall(this, expr) ?? lowerStaticFetchCompanionCall(this, expr) ?? @@ -11774,6 +11967,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/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 3f773c977..b737afcff 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; } @@ -2238,6 +2238,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", @@ -2335,6 +2342,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/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 new file mode 100644 index 000000000..6ec9af036 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -0,0 +1,257 @@ +/* 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?.nodes ?? []; + 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.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( + "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.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]; + 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.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; + return { kind: "intrinsic", name: "slots.set", args: [...args, value], type, loc }; + } + } +} 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 a4ab30105..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; @@ -5980,6 +5987,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.nodes + .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; +} + /** The members and shared value of a CHAINED member export * `exports.a = exports.b = module.exports.c = value` (outermost name * first), or null when the assignment is not a chain of at least two @@ -6024,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; @@ -6062,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) && @@ -6096,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; @@ -6129,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; @@ -6233,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; @@ -6243,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 9d7634591..f3ef51a3d 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. */ @@ -163,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/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/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 0562e673b..128d5fcc2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1068,6 +1068,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; @@ -1112,6 +1115,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): @@ -1346,6 +1352,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 @@ -1369,7 +1409,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 && @@ -1725,16 +1774,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; } } @@ -1997,7 +2051,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; @@ -2009,6 +2063,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. @@ -2046,7 +2106,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); @@ -3246,8 +3306,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; @@ -4268,7 +4329,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 @@ -4318,7 +4379,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 && @@ -4838,9 +4899,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); } @@ -5010,10 +5073,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); @@ -5509,7 +5575,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); @@ -6180,7 +6246,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 @@ -6537,7 +6603,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/index.ts b/packages/compiler/src/index.ts index c616fec93..b2c82767a 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -113,6 +113,7 @@ import { provenanceSources } from "./frontend/provenance-registry.js"; import { clearResolveCaches } from "./frontend/resolve.js"; import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js"; import { executableLinkFeatures } from "./backend/executable-features.js"; +import { youngNurseryRequested } from "./backend/young-nursery.js"; import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js"; import { libraryFrontendImplementationFingerprint, @@ -548,6 +549,7 @@ async function compileExecutableNative( watch: features.watch, foreignFfi: features.foreignFfi, ...(features.workers ? { workers: true } : {}), + ...(features.youngNursery ? { youngNursery: true } : {}), nodeTest: features.nodeTest, tls: features.tls, tlsCa: features.tlsCa, @@ -1047,6 +1049,7 @@ async function compileTracked( ...(opts.strip ? { strip: true as const } : {}), ...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}), ...(opts.detachedExit === true ? { detachedExit: true as const } : {}), + ...(youngNurseryRequested() ? { youngNursery: true as const } : {}), npmStatic: opts.npmStatic ?? null, ffiProfile: opts.ffiProfilePath === undefined || ffiProfileBytes === null 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/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index b72c837ce..fc4c383ff 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 17259bd43..5b2eeee8e 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): @@ -181,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 @@ -1477,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 }, @@ -2285,11 +2291,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 +2311,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ F64, F64, F64, + BOOL, ], result: STRING, }, @@ -3413,6 +3422,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 @@ -3891,7 +3902,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 }, @@ -4205,6 +4216,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 }, @@ -4222,6 +4236,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/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index 0b1f9fb49..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> = { @@ -209,6 +210,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/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/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 90a98a491..6c8df7733 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -879,6 +879,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. @@ -977,6 +1004,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 @@ -1463,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 @@ -1681,6 +1733,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; @@ -2400,13 +2461,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 @@ -3614,7 +3683,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/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 83596b7ea..826189cbb 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, @@ -24,6 +24,12 @@ export interface RuntimeFeatures { assert: boolean; dynInvoke: boolean; dynAsync: boolean; + /** The program enters AsyncLocalStorage stores (store.run, enterWith, + * 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; inspect: boolean; childProcess: boolean; net: boolean; @@ -110,6 +116,19 @@ 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", + // getTestContext() reads the test runner's entry in the same snapshot. + "test.hasContext", + "test.context", +]); + const LEGACY_URL_LIB_FNS: ReadonlySet = new Set([ "url.legacyParse", "url.legacyFormat", @@ -125,6 +144,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). */ @@ -136,10 +315,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 @@ -170,6 +350,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; } @@ -200,6 +389,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run mod.functions.some( (fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn", ), + asyncContext: false, inspect: false, childProcess: false, net: false, @@ -227,131 +417,38 @@ 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") { 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 (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true; if ( fn.startsWith("insp.") || fn.startsWith("console.") || @@ -359,19 +456,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.")) { @@ -384,18 +469,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. @@ -404,8 +483,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. @@ -444,30 +522,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; 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/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 73c819ed9..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 } 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"; @@ -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. */ -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; @@ -998,6 +971,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { errors, mod.workers === true, mod.lifetimeRegions === true, + new Set(mod.threadSlotKeys ?? []), ); } return errors; @@ -1016,6 +990,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) => @@ -2962,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); } @@ -2995,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 })) { @@ -3033,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 })) { @@ -3894,6 +3875,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/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index e43ec6085..54100a413 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -123,6 +123,8 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const features = value as Record; 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 52beef8f9..be07160fd 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/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/surface-manifest.json b/packages/compiler/surface-manifest.json index 809612806..26634df2f 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,26 @@ "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.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", @@ -5293,9 +5319,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", 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/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"); +}); 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/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/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/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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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": [] +} 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/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/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/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/packages/experimental/README.md b/packages/experimental/README.md index 8a44053b0..5ae105aaa 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 057dcde09..f8515f95f 100644 --- a/packages/experimental/index.d.ts +++ b/packages/experimental/index.d.ts @@ -53,3 +53,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 8f63e3ea9..11367335c 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", ]; @@ -125,6 +126,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 f8d1264eb..dacd28937 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", ]; @@ -115,6 +116,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..8badca80f 100644 --- a/packages/runtime-pack-common/test/library-matrix.test.ts +++ b/packages/runtime-pack-common/test/library-matrix.test.ts @@ -61,6 +61,26 @@ 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_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); } diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index 4ef24e906..1f4efc321 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 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) { @@ -4423,6 +4425,10 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) { } void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) { + 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 d02c2097e..5289f8c13 100644 --- a/packages/runtime/src/scr_array.h +++ b/packages/runtime/src/scr_array.h @@ -105,7 +105,12 @@ 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 */ + /* 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 * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like @@ -207,6 +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); +/* 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_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.c b/packages/runtime/src/scr_async.c index f630ee5f9..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,76 @@ 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 + * 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 +1239,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 +3813,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 +3873,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 +4662,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 (;;) { @@ -4892,6 +5037,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; @@ -5453,6 +5599,18 @@ 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"; +} + +__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_async.h b/packages/runtime/src/scr_async.h index efa98e942..d16fd8b9c 100644 --- a/packages/runtime/src/scr_async.h +++ b/packages/runtime/src/scr_async.h @@ -158,6 +158,10 @@ 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); +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) */ @@ -409,6 +413,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 +433,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 95771d457..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,14 +78,10 @@ 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 */ + scr_als_entered_with(); } void scr_als_disable(double id) { @@ -1036,11 +998,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_cycle.c b/packages/runtime/src/scr_cycle.c index a2e7a191b..d700e6ba8 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -444,6 +444,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 static void scr_cyc_route_bypassed(void); /* 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. */ @@ -809,6 +825,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 scr_cyc_route_bypassed(); scr_cyc_note_fall(); if (scr_cyc_buffered() == 0) { @@ -996,6 +1018,13 @@ static SCR_CYC_COMPACT void scr_cyc_route_bypassed(void) { 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. */ @@ -1026,6 +1055,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; @@ -1102,9 +1143,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 @@ -1147,7 +1198,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; @@ -1172,7 +1223,12 @@ static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintp * leaves its buffer here, in the header write that numbers it anyway. A * limited pass can gray candidates it did not take; they keep their * buffer position and are simply never counted as members. */ - if (!scr_cyc_is_buffered(h) || (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take)) + if (!scr_cyc_is_buffered(h) || (scr_gen_limit == SCR_CYC_OLD && !scr_cyc_take +#ifdef SCR_YIN + /* a young object's `buf` is its nursery index */ + && scr_cyc_gen(h) != SCR_CYC_YOUNG +#endif + )) h->buf = scr_gray_comp; if (scr_white.n < SCR_CYC_GRAY_LIST_MAX) scr_cyc_push_gray(&scr_white, obj); if (SCR_RC(obj) > 0) { @@ -1322,6 +1378,340 @@ 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; + /* What died outside passes so far counts toward the death rule; what this + * collection frees does not (the reading is refreshed at its end). */ + scr_cyc_note_fall(); + 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, and take + * the call, as during a pass (scr_cyc_window_update). */ + scr_collecting = true; + scr_cyc_window_update(); + 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_cyc_live_seen = scr_cyc_live; + scr_collecting = false; + scr_cyc_window_update(); + 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_buffered() >= scr_cyc_trigger || scr_cyc_live >= scr_cyc_alloc_trigger) + scr_cyc_collect_due(); + return freed; +} +#endif + /* Dead sets at least this large are torn down in address order. Smaller * ones mostly stay in cache, where ordering them costs more than it saves. */ #define SCR_CYC_ORDER_MIN 16384 @@ -1430,6 +1820,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; @@ -1699,9 +2092,16 @@ void scr_cyc_route_bypassed_at(void) { scr_cyc_route_bypassed(); } 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; } @@ -1715,6 +2115,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()) { } } @@ -1733,6 +2138,24 @@ 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 #if SCR_CYC_INLINE_BUFFERING scr_cyc_nursery_bypassing = 0; #endif 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 ca37aedc5..1b11e619e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -117,77 +117,83 @@ 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_CRYPTO_KEY, /* WebCrypto CryptoKey (scr_webcrypto.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_CRYPTO_KEY = 56, /* WebCrypto CryptoKey (scr_webcrypto.c) */ + 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_MATCH_SOURCE = 55, /* a match result's pending metadata (scr_regex.c) */ + 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) */ @@ -698,6 +704,16 @@ 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); +/* 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. */ @@ -877,6 +893,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 */ @@ -999,6 +1018,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_fetch.c b/packages/runtime/src/scr_fetch.c index eff2ae6bc..17beb9122 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) { @@ -2386,6 +2411,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 +2507,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 = @@ -4415,14 +4479,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); } @@ -4578,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; @@ -4594,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) { @@ -4668,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 && @@ -4719,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(); @@ -4744,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; @@ -4809,6 +4949,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; } @@ -5118,6 +5259,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/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index f0ecb9d85..c4397e021 100644 --- a/packages/runtime/src/scr_generator.h +++ b/packages/runtime/src/scr_generator.h @@ -63,6 +63,20 @@ 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); +/* 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); @@ -166,6 +180,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/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/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 34438353f..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); } @@ -801,14 +823,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 +937,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 +1014,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 +1022,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 +1038,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; } @@ -993,6 +1080,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_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); + 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"; @@ -1244,6 +1424,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 @@ -1284,6 +1468,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) { @@ -1301,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. */ @@ -1312,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 f8d28b4f0..4eb607a5c 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. */ @@ -50,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/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 401933904..525b6a192 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; @@ -2568,10 +2570,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, @@ -4657,6 +4675,31 @@ 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; + 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); @@ -6186,6 +6229,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_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); + 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); @@ -6218,6 +6275,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); @@ -6392,10 +6456,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 +6477,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 +6515,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 +6634,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 +6676,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 +6757,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 @@ -6805,6 +6962,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 +7009,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 +7433,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 +7461,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 +7477,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 +7610,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 +7634,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) { @@ -7741,11 +7921,27 @@ 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; } +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_lib.c b/packages/runtime/src/scr_lib.c index af57ab057..ae9c5c2b1 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()); } @@ -10489,6 +10487,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 @@ -10682,6 +10746,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 +10785,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 +10869,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 +10909,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 +11603,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 +11669,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_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; diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index f980daeb5..2d95d95f5 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -102,6 +102,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); @@ -120,6 +123,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_rc.h b/packages/runtime/src/scr_rc.h index faac6987a..eb28fb2e7 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -607,4 +607,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/src/scr_regex.c b/packages/runtime/src/scr_regex.c index a2e591b0e..44a91410b 100644 --- a/packages/runtime/src/scr_regex.c +++ b/packages/runtime/src/scr_regex.c @@ -610,6 +610,138 @@ 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 + * 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; + ScrRegex *re; + ScrDyn *input_value; /* `input` as a dynamic value, built on first use */ +} ScrRegexMatchSource; + +static bool scr_regex_copy_pending_metadata(ScrArr *a, ScrDyn *target); + +static void *scr_regex_source_retain_v(void *p) { + ((ScrRegexMatchSource *)p)->rc++; + return p; +} + +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); +} + +/* 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); + 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); +} + +/* 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) { + ScrRegexMatchSource *src = scr_regex_pending(a); + if (!src) return; + 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) { + 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(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) { @@ -713,12 +845,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 +876,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) { + 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); return out; /* +1, or NULL (no match) */ @@ -883,8 +1014,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 +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); + ScrDyn *source = metadata ? scr_regex_source_new(s, re) : NULL; ScrRegexInput input; scr_regex_input_init(&input, re, bc, s); int len = input.len; @@ -906,30 +1041,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 (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_dyn_release(source); 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 014e5aa83..01c545bc5 100644 --- a/packages/runtime/src/scr_regex.h +++ b/packages/runtime/src/scr_regex.h @@ -98,11 +98,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/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; 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/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index cc52513a6..7c5c243d8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -125,6 +125,8 @@ 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 */ + size_t serial; /* index in scr_test_nodes, getTestContext()'s handle */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -167,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; } @@ -203,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 @@ -230,12 +249,44 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->plan = -1; 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; @@ -391,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 = @@ -410,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. @@ -432,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; @@ -597,6 +652,81 @@ 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 */ + ScrTestCtx **owners; /* owned refs: each afterEach hook's suite or test */ +} 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, 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); +} + +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); + 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; + 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. 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]; + 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); + scr_test_mocks_restore(&c->mocks, true); + scr_mt_reset(c->mock_timers); + scr_test_cancelled_hooks(c); + } +} + static void scr_test_fail_unrun(ScrTestCtx *c, ScrCaught *err) { double start = scr_now_ms(); c->err = scr_caught_retain(err); @@ -689,6 +819,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 @@ -702,11 +833,22 @@ 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 + * (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; + } 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 @@ -1619,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); @@ -1636,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 @@ -1652,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); @@ -1660,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); } @@ -1738,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(); @@ -2195,6 +2342,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); @@ -2310,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 @@ -2325,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 64a280bf7..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 @@ -110,6 +114,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/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/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/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/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/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/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", 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/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/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/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/corpus/5954-generic-optional-returns.ts b/tests/corpus/5954-generic-optional-returns.ts new file mode 100644 index 000000000..3f8309c9d --- /dev/null +++ b/tests/corpus/5954-generic-optional-returns.ts @@ -0,0 +1,66 @@ +// 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)); + +// 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(); 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/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/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-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/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 }); +} 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/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/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/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/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/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/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; 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" } 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); 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')); 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); +} 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')); 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]); +} 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/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'))); 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" } 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" } 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); 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); 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/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); 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")); 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); 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); 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/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]]))); 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}`)); 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(); 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(); +}); 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)); 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); 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); 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); 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(",")); 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" })); 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/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/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/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/date-formatters.ts b/tests/diagnostics/date-formatters.ts index 6a2cb57a7..e803b773f 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.toLocaleTimeString(); date.toLocaleString(); 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/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/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/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/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/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/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: ", + ]); +}); 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..b2f61dd89 --- /dev/null +++ b/tests/fixtures/node-test/cases/test-name-forms.ts @@ -0,0 +1,55 @@ +// 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 "". 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"; + +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); +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" }, () => {}); 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", + ]); +}); 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); +}); 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/__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__/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 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__/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__/date-formatters.ts.txt b/tests/harness/__snapshots__/date-formatters.ts.txt index 66d98d861..75e15760b 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.toLocaleTimeString' is part of the standard library types but has no scriptc lowering yet 3 | const date = new Date(0); - 4 | date.toString(); - | ^~~~~~~~~~~~~~~ + 4 | date.toLocaleTimeString(); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ 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(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters 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.toLocaleTimeString(); 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(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering \ 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__/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/__snapshots__/stdlib-fence.ts.txt b/tests/harness/__snapshots__/stdlib-fence.ts.txt index 337354eec..b813840d8 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(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters 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/__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/__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 a3ee81f9b..62703f6a4 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, @@ -2004,6 +2007,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, @@ -2023,6 +2027,9 @@ "5724-intern-table-array-keys.ts": 4072, "5725-intern-table-callbacks.ts": 1996, "5726-intern-table-key-limits.ts": 2644, + "5727-thread-slots-basics.ts": 3100, + "5728-thread-slots-workers.ts": 2500, + "5729-thread-slots-values.ts": 2550, "5740-atomics-operation-semantics.ts": 4810, "5741-atomics-contended-counters.ts": 2504, "5742-atomics-lock-ordering.ts": 3000, @@ -2085,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, @@ -2195,6 +2203,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, @@ -2206,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, @@ -2216,7 +2228,11 @@ "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, + "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, @@ -2227,7 +2243,15 @@ "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, + "6071-iterator-sources.ts": 5150, + "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, @@ -2248,6 +2272,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, @@ -5046,6 +5071,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, @@ -5068,6 +5096,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, @@ -5087,6 +5116,9 @@ "5724-intern-table-array-keys.ts": 43939, "5725-intern-table-callbacks.ts": 3924, "5726-intern-table-key-limits.ts": 3414, + "5727-thread-slots-basics.ts": 4500, + "5728-thread-slots-workers.ts": 5000, + "5729-thread-slots-values.ts": 4400, "5740-atomics-operation-semantics.ts": 4541, "5741-atomics-contended-counters.ts": 5176, "5742-atomics-lock-ordering.ts": 44911, @@ -5149,6 +5181,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, @@ -5259,6 +5292,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, @@ -5270,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, @@ -5280,7 +5317,11 @@ "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, + "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, @@ -5291,7 +5332,15 @@ "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, + "6071-iterator-sources.ts": 5150, + "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, @@ -5312,6 +5361,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, 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, + ); + } + }); +}); 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)), diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index fc243ccb5..c7cb13751 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -551,7 +551,11 @@ 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.toUTCString", "stdlib.date.valueOf", ]); expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c22e8a839..fa71b5a08 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -42,6 +42,7 @@ const features: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("native toolchain configuration, staging and helper validation are static", async () => { diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 08eb45754..c95d6d9a0 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -43,6 +43,7 @@ const base: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("runtime manifest validation and selection are entirely static", async () => { 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); } 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-");