diff --git a/.gitignore b/.gitignore index 4922ef6df..7b7bc57f2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ output/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ !tests/fixtures/provenance/node_modules/*/dist/ !tests/fixtures/npm-static/node_modules/*/dist/ +!tests/corpus/*/node_modules/*/dist/ .scriptc/ *.log .DS_Store diff --git a/AGENTS.md b/AGENTS.md index 2941691bc..ba32fe688 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways. +Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/.json`, not in `tests/harness/corpus-costs.json`; see the README there. + Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep. A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared: diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 805d7f612..b27327b91 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -76,7 +76,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them. - `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity. -- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. +- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output. - `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation. - `SC6004`: a call passes a class instance to a function that casts it to an unrelated class. - `SC6005`: a `Date` string literal uses a form the native parser does not accept. 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..341994faa --- /dev/null +++ b/docs/content/limitations/builtin-apis/asynclocalstorage.mdx @@ -0,0 +1,2 @@ +- **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 934f60615..4c1c7e034 100644 --- a/docs/content/limitations/builtin-apis/date.mdx +++ b/docs/content/limitations/builtin-apis/date.mdx @@ -1,5 +1,5 @@ -- **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. +- **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 285a60694..39031bf8e 100644 --- a/docs/content/limitations/builtin-apis/iterator-helpers.mdx +++ b/docs/content/limitations/builtin-apis/iterator-helpers.mdx @@ -3,5 +3,7 @@ In TypeScript, `h.return!()` and `h.return?.()` compile when `h` is a helper call or a `const` initialized with one. `flatMap` callbacks return arrays, generators, Sets or Maps; a string result throws Node's TypeError. `Iterator.from` accepts arrays, strings, typed arrays, Sets, Maps and iterator objects such as generators. - In JavaScript, untyped array, string, Map and Set iterators answer the same methods. - `Iterator.from` over an object with its own `next` method or over an untyped value, the `Iterator` global as a value, and printing typed helper objects are unsupported. + In JavaScript, untyped generators and array, string, Map and Set iterators answer the same methods, and array destructuring of untyped values steps generators, iterators, Maps and Sets the way Node does. + Helper methods on untyped async generators are unsupported. + `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 aad05d5c2..b25eb0eaa 100644 --- a/docs/content/limitations/builtin-apis/test-runner.mdx +++ b/docs/content/limitations/builtin-apis/test-runner.mdx @@ -21,3 +21,10 @@ - **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 801aa1a6a..e5655eb85 100644 --- a/docs/content/limitations/dates-and-timezones.mdx +++ b/docs/content/limitations/dates-and-timezones.mdx @@ -4,3 +4,10 @@ Other V8-specific forms and offset-less local date-times produce an Invalid Date 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/record-identity-and-property-order.mdx b/docs/content/limitations/record-identity-and-property-order.mdx index 799742fbf..0381803f6 100644 --- a/docs/content/limitations/record-identity-and-property-order.mdx +++ b/docs/content/limitations/record-identity-and-property-order.mdx @@ -10,9 +10,9 @@ A type whose values are only read field by field, from fields the class does not Object spread of a class instance is refused. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference. -`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. -Their order matches Node.js when objects are built in declaration order. -`SC6002` reports object literals whose order differs where the program observes it. +`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with. +The order matches Node.js when every value of the type is created in the same order. +When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it. A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare. `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order. diff --git a/docs/content/limitations/strings-and-sorting.mdx b/docs/content/limitations/strings-and-sorting.mdx index 43a8a04c7..499a3f950 100644 --- a/docs/content/limitations/strings-and-sorting.mdx +++ b/docs/content/limitations/strings-and-sorting.mdx @@ -1,5 +1,5 @@ Strings are stored as UTF-8, with lone surrogates kept as their own code units. -Length and string methods use UTF-16 semantics, including for lone surrogates, but relational comparisons such as `<` and `>` use code-point order. +Length, string methods and relational comparisons such as `<` and `>` use UTF-16 semantics, including for lone surrogates. `split` with a string separator and `replace` or `replaceAll` with a string pattern don't match a lone surrogate against half of a surrogate pair; regular-expression patterns do. Output, file contents, and other encoded text write each lone surrogate as U+FFFD, as Node does. File system paths and network host names that contain a lone surrogate reach the operating system unconverted. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json new file mode 100644 index 000000000..167256bca --- /dev/null +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 3, + "nodeVersion": "24.22.0", + "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", + "artifactVersion": "ee72820d45a79aa154f0", + "rowCount": 3880 +} diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 0e6387ea9..be1a931cc 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -208,10 +208,10 @@ "entries": 152, "apiEntries": 152, "static": { - "supported": 5, + "supported": 6, "partial": 39, "refused": 21, - "not-implemented": 87, + "not-implemented": 86, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -314,9 +314,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 198, + "partial": 200, "refused": 0, - "not-implemented": 75, + "not-implemented": 73, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -328,10 +328,10 @@ "entries": 92, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -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 @@ -853,10 +853,10 @@ "entries": 156, "apiEntries": 156, "static": { - "supported": 1, - "partial": 10, + "supported": 2, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -12325,9 +12325,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -30288,9 +30288,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 call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -35256,9 +35256,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 call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -35693,9 +35693,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" } }, { @@ -36360,9 +36360,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" } }, { @@ -37165,9 +37165,9 @@ "inherited": false }, "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" } }, { @@ -37303,9 +37303,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -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" } }, { @@ -80183,9 +80183,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -80229,9 +80229,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" } }, { @@ -80252,9 +80252,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" } }, { @@ -80275,9 +80275,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" } }, { @@ -80298,9 +80298,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" } }, { @@ -80321,9 +80321,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" } }, { @@ -80344,9 +80344,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" } }, { @@ -80436,9 +80436,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" } }, { @@ -80574,9 +80574,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" } }, { @@ -80735,9 +80735,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" } }, { @@ -80758,9 +80758,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" } }, { @@ -80804,9 +80804,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 7d0fcf75c..a02446908 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -3,21 +3,21 @@ "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", "summary": { - "taskCount": 3456, - "tierItemCount": 3456, + "taskCount": 3453, + "tierItemCount": 3453, "tiers": { "static": { - "audit-partial": 1162, - "verify-gap": 2166, + "audit-partial": 1179, + "verify-gap": 2146, "replace-refusal": 41, "classify": 2, "implement": 85 } }, "priorities": { - "high": 2289, + "high": 2287, "normal": 694, - "low": 473 + "low": 472 } }, "tasks": [ @@ -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" ] } } @@ -7352,31 +7359,6 @@ } } }, - { - "id": "crypto:2ab8394e29a7", - "chapter": "crypto", - "label": "crypto.getRandomValues(typedArray)", - "signature": "`crypto.getRandomValues(typedArray)`", - "apiSymbol": "crypto.getRandomValues", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/crypto.html#cryptogetrandomvaluestypedarray", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:crypto", - "tests": [] - } - } - }, { "id": "crypto:21f22c71fb61", "chapter": "crypto", @@ -27060,12 +27042,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] } } }, @@ -32757,12 +32742,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:fs", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] } } }, @@ -33213,12 +33201,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -33944,12 +33938,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -34865,12 +34865,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] } } }, @@ -34999,31 +35003,6 @@ } } }, - { - "id": "globals:fc0a32e8c26f", - "chapter": "globals", - "label": "userAgent", - "signature": "`userAgent` Type: {string}", - "apiSymbol": "userAgent", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator", - "nodeStability": { - "index": "1.1", - "level": 1, - "text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.", - "inherited": true - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] - } - } - }, { "id": "globals:b412bf9a456d", "chapter": "globals", @@ -68875,12 +68854,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" + ] } } }, @@ -81016,31 +80999,6 @@ } } }, - { - "id": "webcrypto:dcc24f369f43", - "chapter": "webcrypto", - "label": "crypto.getRandomValues(typedArray)", - "signature": "`crypto.getRandomValues(typedArray)`", - "apiSymbol": "crypto.getRandomValues", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/webcrypto.html#cryptogetrandomvaluestypedarray", - "nodeStability": { - "index": "2", - "level": 2, - "text": "Stable", - "inherited": true - }, - "priority": "high", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] - } - } - }, { "id": "webcrypto:8c570ec75666", "chapter": "webcrypto", @@ -81057,12 +81015,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81082,12 +81046,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81107,12 +81075,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81132,12 +81104,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81157,12 +81133,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81182,12 +81162,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81282,12 +81266,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81432,12 +81422,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81607,12 +81604,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81632,12 +81636,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81682,12 +81693,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c87f98fec..3690ce077 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -190,10 +190,10 @@ "entries": 179, "apiEntries": 152, "static": { - "supported": 5, + "supported": 6, "partial": 39, "refused": 21, - "not-implemented": 87, + "not-implemented": 86, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -326,9 +326,9 @@ "apiEntries": 280, "static": { "supported": 7, - "partial": 198, + "partial": 200, "refused": 0, - "not-implemented": 75, + "not-implemented": 73, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -340,10 +340,10 @@ "entries": 94, "apiEntries": 92, "static": { - "supported": 0, - "partial": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -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 @@ -910,10 +910,10 @@ "entries": 176, "apiEntries": 156, "static": { - "supported": 1, - "partial": 10, + "supported": 2, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 133, "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" @@ -19863,6 +19872,8 @@ "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -21019,8 +21030,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "supported", + "evidence": "compiler-dedicated:crypto.getRandomValues", + "tests": [ + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" + ] }, "anchorSource": "exact" }, @@ -44129,6 +44144,8 @@ "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts", + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs", "tests/corpus/fs-native-realpath.ts", "tests/corpus/node24-fs-links-callbacks.cjs", "tests/corpus/node24-fs-links-errors.cjs", @@ -45916,8 +45933,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] }, "anchorSource": "ancestor" }, @@ -51788,8 +51809,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:fs" + "status": "partial", + "evidence": "surface-manifest:node-builtin.fs.constants", + "tests": [ + "tests/corpus/6207-fs-constants-object.ts", + "tests/corpus/6208-fs-constants-require.cjs" + ] }, "anchorSource": "ancestor" }, @@ -52217,8 +52242,16 @@ "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js", "tests/corpus/5830-timers-promises-signal.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/6331-jose-root-entry/main.ts", "tests/corpus/text-codec-values/main.ts", - "tests/harness/fetch-conformance.test.ts" + "tests/fixtures/node-types/navigator.ts", + "tests/harness/fetch-conformance.test.ts", + "tests/harness/project-config.test.ts" ] }, "anchorSource": "chapter" @@ -52639,8 +52672,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -53313,8 +53353,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -54219,8 +54266,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] }, "anchorSource": "exact" }, @@ -54357,8 +54409,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.userAgent", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts" + ] }, "anchorSource": "ancestor" }, @@ -91046,6 +91102,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", @@ -94794,8 +94852,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" }, @@ -106838,10 +106901,18 @@ "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", + "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110322,8 +110393,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "supported", + "evidence": "compiler-dedicated:crypto.getRandomValues", + "tests": [ + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" + ] }, "anchorSource": "exact" }, @@ -110373,8 +110448,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110396,8 +110478,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110419,8 +110506,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110442,8 +110534,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110465,8 +110562,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110488,8 +110590,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110580,8 +110687,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110718,8 +110832,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110879,8 +111001,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110902,8 +111032,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110948,8 +111086,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 584492bf1..855d065f7 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"], @@ -111,6 +111,8 @@ "node-builtin.os.uptime": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"], "node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"], + "node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"], + "node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"], "node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], "node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], "node-builtin.url.resolve": ["tests/corpus/5226-url-resolve.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"], @@ -177,11 +179,19 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], + "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, + { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, + { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, {"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]}, @@ -366,6 +376,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"] }, @@ -390,12 +401,13 @@ { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, { "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] }, { "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] }, + { "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] }, { "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] }, { "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 9655b7580..a8f1f8a8b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -511,14 +511,119 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* Capability-gated globals harness code probes behind feature guards - * (globalThis.crypto under a hasCrypto check, the SQLite-backed storage - * pair): typed `unknown` — no lowering exists, the guard is expected to - * be false in a compiled binary, and any reached use fences per site. */ -declare var crypto: unknown; +/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for + * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as + * exactly that surface (the declarations mirror @types/node's webcrypto + * shapes). Every other SubtleCrypto method and crypto member is absent, + * so a use is an honest type error. CryptoKey values are native handles + * whose four members read through the handle. */ +declare var crypto: Crypto; +/* The Crypto and SubtleCrypto constructors (feature probes only) stay + * `unknown`; the values keep this declaration order. */ declare var Crypto: unknown; -declare var CryptoKey: unknown; +declare var CryptoKey: { + prototype: CryptoKey; + new (): CryptoKey; +}; declare var SubtleCrypto: unknown; +type KeyUsage = + | "encrypt" + | "decrypt" + | "sign" + | "verify" + | "deriveKey" + | "deriveBits" + | "encapsulateBits" + | "decapsulateBits" + | "encapsulateKey" + | "decapsulateKey" + | "wrapKey" + | "unwrapKey"; +type KeyType = "private" | "public" | "secret"; +interface Algorithm { + name: string; +} +type AlgorithmIdentifier = Algorithm | string; +type HashAlgorithmIdentifier = AlgorithmIdentifier; +interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface KeyAlgorithm { + name: string; +} +interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; +} +interface CryptoKey { + readonly algorithm: KeyAlgorithm; + readonly extractable: boolean; + readonly type: KeyType; + readonly usages: KeyUsage[]; +} +type KeyFormat = + | "jwk" + | "pkcs8" + | "raw" + | "raw-private" + | "raw-public" + | "raw-secret" + | "raw-seed" + | "spki"; +interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} +/* The 'jwk' form type-checks like @types/node's; the compiler refuses it + * (the JsonWebKey import is not implemented). */ +interface SubtleCrypto { + digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + importKey( + format: Exclude, + keyData: BufferSource, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + verify( + algorithm: AlgorithmIdentifier, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise; +} +interface Crypto { + readonly subtle: SubtleCrypto; +} + +/* Capability-gated globals harness code probes behind feature guards + * (the SQLite-backed storage pair): typed `unknown` — no lowering exists, + * the guard is expected to be false in a compiled binary, and any reached + * use fences per site. */ declare var localStorage: unknown; declare var sessionStorage: unknown; @@ -1493,6 +1598,51 @@ declare module "node:fs" { readonly COPYFILE_EXCL: number; readonly COPYFILE_FICLONE: number; readonly COPYFILE_FICLONE_FORCE: number; + readonly UV_FS_SYMLINK_DIR: number; + readonly UV_FS_SYMLINK_JUNCTION: number; + readonly UV_DIRENT_UNKNOWN: number; + readonly UV_DIRENT_FILE: number; + readonly UV_DIRENT_DIR: number; + readonly UV_DIRENT_LINK: number; + readonly UV_DIRENT_FIFO: number; + readonly UV_DIRENT_SOCKET: number; + readonly UV_DIRENT_CHAR: number; + readonly UV_DIRENT_BLOCK: number; + readonly UV_FS_O_FILEMAP: number; + readonly UV_FS_O_TEMPORARY: number; + readonly UV_FS_O_SHORT_LIVED: number; + readonly UV_FS_O_SEQUENTIAL: number; + readonly UV_FS_O_RANDOM: number; + readonly UV_FS_COPYFILE_EXCL: number; + readonly UV_FS_COPYFILE_FICLONE: number; + readonly UV_FS_COPYFILE_FICLONE_FORCE: number; + readonly S_IFMT: number; + readonly S_IFREG: number; + readonly S_IFDIR: number; + readonly S_IFCHR: number; + readonly S_IFBLK: number; + readonly S_IFIFO: number; + readonly S_IFLNK: number; + readonly S_IFSOCK: number; + readonly O_NOCTTY: number; + readonly O_DIRECTORY: number; + readonly O_NOATIME: number; + readonly O_SYNC: number; + readonly O_DSYNC: number; + readonly O_SYMLINK: number; + readonly O_DIRECT: number; + readonly S_IRWXU: number; + readonly S_IRUSR: number; + readonly S_IWUSR: number; + readonly S_IXUSR: number; + readonly S_IRWXG: number; + readonly S_IRGRP: number; + readonly S_IWGRP: number; + readonly S_IXGRP: number; + readonly S_IRWXO: number; + readonly S_IROTH: number; + readonly S_IWOTH: number; + readonly S_IXOTH: number; }; /* A stat(2) snapshot (statSync follows symlinks, lstatSync does not — * Node's split) — immutable; the supported surface is exactly these @@ -2557,6 +2707,23 @@ declare module "crypto" { a: ArrayBufferView | ArrayBufferLike, b: ArrayBufferView | ArrayBufferLike, ): boolean; + /* WebCrypto's getRandomValues (also the webcrypto object's): integer + * typed arrays, filled in place and returned. The crypto GLOBAL stays + * unknown in fallback builds. */ + type RandomValuesArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array; + export function getRandomValues(typedArray: T): T; + export const webcrypto: { + getRandomValues(typedArray: T): T; + }; export function randomFillSync( buffer: T, offset?: number, @@ -3405,8 +3572,10 @@ declare module "node:test" { readonly tags: readonly string[]; readonly assert: TestContextAssert; readonly mock: MockTracker; - test(name: string, fn?: TestFn): Promise; - test(name: string, options: TestOptions, fn?: TestFn): Promise; + test(name?: string, fn?: TestFn): Promise; + test(name?: string, options?: TestOptions, fn?: TestFn): Promise; + test(options?: TestOptions, fn?: TestFn): Promise; + test(fn?: TestFn): Promise; skip(message?: string): void; todo(message?: string): void; diagnostic(message: string): void; @@ -3436,23 +3605,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; @@ -3527,6 +3717,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. */ @@ -3544,6 +3737,7 @@ declare module "node:test" { afterEach, mock, assert, + getTestContext, run, snapshot, TestContext, diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 553643470..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, @@ -38,5 +40,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, }; } diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 833ef93e7..e1fd9784b 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -52,6 +52,7 @@ export async function compilePackedLibrary( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }; const selection = await loadRuntimePack({ 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 02036e4d9..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 @@ -850,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); @@ -926,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[] = []; @@ -992,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.ts b/packages/compiler/src/backend/llvm/dyn.ts index 154b80c02..cf7578c02 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -4014,14 +4014,9 @@ export class LlDyn { const name = "sc_dyn_iter_n"; this.dynBuilders.set(memoKey, name); const host = this.host; - host.declare(`declare void @scr_jb_init(ptr)`); - host.declare(`declare ptr @scr_jb_finish(ptr)`); - host.declare(`declare void @scr_jb_puts(ptr, ptr)`); - host.declare(`declare void @scr_throw_error(i32, ptr)`); host.declare(`declare ptr @scr_dyn_new_arr()`); host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`); host.declare(`declare ptr @scr_dyn_new_num(double)`); - host.declare(`declare ptr @scr_f64_to_scrstr(double)`); host.declare(`declare void @scr_str_release(ptr)`); const B = new BlockBuilder(); const kd = this.kindOf(B, "%d"); @@ -4033,6 +4028,21 @@ export class LlDyn { const lNext = B.newLabel("din.nt"); B.condBr(isTyped, lTyped, lNext); B.startBlock(lTyped); + // Compiled generators step through the iterator protocol. + host.declare(`declare i1 @scr_dyn_iterates_by_protocol(ptr)`); + host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`); + const protocol = B.tmp(); + const lProtocol = B.newLabel("din.pr"); + const lView = B.newLabel("din.vw"); + B.line(`${protocol} = call i1 @scr_dyn_iterates_by_protocol(ptr %d)`); + B.condBr(protocol, lProtocol, lView); + B.startBlock(lProtocol); + const stepped = B.tmp(); + B.line( + `${stepped} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`, + ); + B.terminate(`ret ptr ${stepped}`); + B.startBlock(lView); host.declare(`declare ptr @scr_dyn_typed_ref_materialize(ptr)`); host.declare(`declare void @scr_dyn_release_v(ptr)`); const materialized = B.tmp(); @@ -4057,71 +4067,13 @@ export class LlDyn { const lGo = B.newLabel("din.g"); const lThrow = B.newLabel("din.t"); B.condBr(ok, lGo, lThrow); - // V8's exact not-iterable TypeError. + // Other iterables step through the iterator protocol; non-iterables + // throw V8's exact not-iterable TypeError. B.startBlock(lThrow); - const buf = "%dib"; - B.entryAllocas.push(`${buf} = alloca %ScrJsonBuf`); - B.line(`call void @scr_jb_init(ptr ${buf})`); - const hasSpelling = B.tmp(); - B.line(`${hasSpelling} = icmp ne ptr %spelling, null`); - const named = B.newLabel("din.named"), - unnamed = B.newLabel("din.unnamed"); - B.condBr(hasSpelling, named, unnamed); - B.startBlock(named); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr %spelling)`); - const namedMessage = B.tmp(); - B.line(`${namedMessage} = call ptr @scr_jb_finish(ptr ${buf})`); - B.line(`call void @scr_throw_error(i32 1, ptr ${namedMessage})`); - B.terminate(`ret ptr null`); - B.startBlock(unnamed); - const lU = B.newLabel("din.u"); - const lN = B.newLabel("din.n"); - const lB2 = B.newLabel("din.b"); - const lNum = B.newLabel("din.m"); - const lF = B.newLabel("din.f"); - const lDef = B.newLabel("din.d"); - const lTail = B.newLabel("din.e"); - B.terminate( - `switch i32 ${kd}, label %${lDef} [ i32 ${DYN_KIND.UNDEF}, label %${lU} i32 ${DYN_KIND.NULL}, label %${lN} i32 ${DYN_KIND.BOOL}, label %${lB2} i32 ${DYN_KIND.NUM}, label %${lNum} i32 ${DYN_KIND.FUNC}, label %${lF} ]`, - ); - B.startBlock(lU); - this.puts(B, buf, "undefined"); - B.br(lTail); - B.startBlock(lN); - this.puts(B, buf, "object null"); - B.br(lTail); - B.startBlock(lB2); - { - const bv = this.boolOf(B, "%d"); - const s = B.tmp(); - B.line( - `${s} = select i1 ${bv}, ptr ${host.cstr("boolean true")}, ptr ${host.cstr("boolean false")}`, - ); - B.line(`call void @scr_jb_puts(ptr ${buf}, ptr ${s})`); - B.br(lTail); - } - B.startBlock(lNum); - { - this.puts(B, buf, "number "); - const x = this.payloadOf(B, "%d", "double"); - const s = B.tmp(); - B.line(`${s} = call ptr @scr_f64_to_scrstr(double ${x})`); - this.putScrStr(B, buf, s); - B.line(`call void @scr_str_release(ptr ${s})`); - B.br(lTail); - } - B.startBlock(lF); - this.puts(B, buf, "function"); - B.br(lTail); - B.startBlock(lDef); - this.puts(B, buf, "object"); - B.br(lTail); - B.startBlock(lTail); - this.puts(B, buf, " is not iterable (cannot read property Symbol(Symbol.iterator))"); - const msg = B.tmp(); - B.line(`${msg} = call ptr @scr_jb_finish(ptr ${buf})`); - B.line(`call void @scr_throw_error(i32 1, ptr ${msg}) ; SCR_ERR_TYPE`); - B.terminate(`ret ptr null`); + host.declare(`declare ptr @scr_dyn_iter_take(ptr, ${host.sizeType}, ptr)`); + const taken = B.tmp(); + B.line(`${taken} = call ptr @scr_dyn_iter_take(ptr %d, ${host.sizeType} %n, ptr %spelling)`); + B.terminate(`ret ptr ${taken}`); B.startBlock(lGo); const out = B.tmp(); B.line(`${out} = call ptr @scr_dyn_new_arr()`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 588e8c26a..a16f129ef 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -11,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"; @@ -56,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"; @@ -968,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(); @@ -1454,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(), @@ -3660,13 +3671,30 @@ export class LlEmitter { } defs.push(...helper.define(this.sizeType, FN_ATTRS, scanInline), ``); } - // Ordering also decides inline when the first bytes differ: byte order - // is the plain comparison's order, and the UTF-16 order whenever both - // bytes are ASCII. Callers only test the sign of the result. - for (const fn of ["scr_str_cmp", "scr_str_cmp_u16"]) { - if (!this.decls.has(`declare i32 @${fn}(ptr, ptr)`)) continue; + // Ordering decides inline from the first differing bytes. The bytes + // before them are equal, so both strings are at the same position of a + // character: either both bytes continue characters with the same lead + // byte, or both start a character. Byte order is the UTF-16 order for + // two continuation bytes, and for two lead bytes when one starts a + // character below U+D000, whose UTF-16 unit is below every unit of a + // character with a lead byte of at least 0xED. Between lead bytes of at + // least 0xED, a supplementary character (0xF0..0xF4, a high surrogate + // unit) orders below U+E000..U+FFFF (0xEE, 0xEF), and a lead byte of + // 0xED (U+D000..U+D7FF or a lone surrogate) takes the runtime. + // Differing words without a byte of at least 0xED decide in byte + // order; other differing words decide by their first differing byte, + // and strings shorter than a word by their first byte. Otherwise a + // proven-ASCII string on either side orders by bytes (scr_str_cmp), and + // other pairs call scr_str_cmp_u16_bytes, which only reads the two + // strings, so LLVM can merge repeated comparisons of one pair. Callers + // only test the sign of the result. + if (this.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) { + const fn = "scr_str_cmp_u16"; this.declare("declare i64 @llvm.bswap.i64(i64)"); - const utf16 = fn === "scr_str_cmp_u16"; + this.declare("declare i64 @llvm.cttz.i64(i64, i1 immarg)"); + this.declare("declare i8 @llvm.umin.i8(i8, i8)"); + this.declare("declare i32 @scr_str_cmp(ptr, ptr)"); + this.declare("declare i32 @scr_str_cmp_u16_bytes(ptr, ptr)"); defs.push( `define internal i32 @${fn.replace(/^scr_/, "sc_")}(ptr %a, ptr %b) ${FN_ATTRS} {`, `entry:`, @@ -3681,8 +3709,7 @@ export class LlEmitter { ` %bempty = icmp eq ${this.sizeType} %blen, 0`, ` %empty = or i1 %aempty, %bempty`, ` br i1 %empty, label %order, label %words`, - // Both at least 8 bytes: the first differing byte of one word pair. - // Byte-swapped little-endian words compare in byte order. + // Both at least 8 bytes: the first word of each. `words:`, ` %along = icmp uge ${this.sizeType} %alen, 8`, ` %blong = icmp uge ${this.sizeType} %blen, 8`, @@ -3694,22 +3721,7 @@ export class LlEmitter { ` %aw = load i64, ptr %awp, align 1`, ` %bw = load i64, ptr %bwp, align 1`, ` %wdiffer = icmp ne i64 %aw, %bw`, - ` br i1 %wdiffer, label %wdiff, label %wsame`, - `wdiff:`, - ...(utf16 - ? [ - ` %wbits = or i64 %aw, %bw`, - ` %whigh = and i64 %wbits, -9187201950435737472`, - ` %wascii = icmp eq i64 %whigh, 0`, - ` br i1 %wascii, label %wordorder, label %order`, - ] - : [` br label %wordorder`]), - `wordorder:`, - ` %asw = call i64 @llvm.bswap.i64(i64 %aw)`, - ` %bsw = call i64 @llvm.bswap.i64(i64 %bw)`, - ` %wbelow = icmp ult i64 %asw, %bsw`, - ` %wsign = select i1 %wbelow, i32 -1, i32 1`, - ` ret i32 %wsign`, + ` br i1 %wdiffer, label %differ, label %wsame`, // Equal first words and one length of at most 16 bytes: the word // ending at that length covers the rest, and any difference lies // past the equal first word. @@ -3725,42 +3737,89 @@ export class LlEmitter { ` %at = load i64, ptr %atp, align 1`, ` %bt = load i64, ptr %btp, align 1`, ` %tdiffer = icmp ne i64 %at, %bt`, - ` br i1 %tdiffer, label %tdiff, label %equal`, - `tdiff:`, - ...(utf16 - ? [ - ` %tbits = or i64 %at, %bt`, - ` %thigh = and i64 %tbits, -9187201950435737472`, - ` %tascii = icmp eq i64 %thigh, 0`, - ` br i1 %tascii, label %tailorder, label %order`, - ] - : [` br label %tailorder`]), - `tailorder:`, - ` %ats = call i64 @llvm.bswap.i64(i64 %at)`, - ` %bts = call i64 @llvm.bswap.i64(i64 %bt)`, - ` %tbelow = icmp ult i64 %ats, %bts`, - ` %tsign = select i1 %tbelow, i32 -1, i32 1`, - ` ret i32 %tsign`, + ` br i1 %tdiffer, label %differ, label %equal`, `first:`, ` %adata = getelementptr inbounds %ScrStr, ptr %a, i32 1`, ` %bdata = getelementptr inbounds %ScrStr, ptr %b, i32 1`, ` %x = load i8, ptr %adata`, ` %y = load i8, ptr %bdata`, - ` %differ = icmp ne i8 %x, %y`, - ...(utf16 - ? [ - ` %bits = or i8 %x, %y`, - ` %ascii = icmp sge i8 %bits, 0`, - ` %decide = and i1 %differ, %ascii`, - ] - : [` %decide = and i1 %differ, true`]), - ` br i1 %decide, label %byte, label %order`, + ` %fdiffer = icmp ne i8 %x, %y`, + ` br i1 %fdiffer, label %bytes, label %order`, + // Two differing words. When neither has a byte of at least 0xED + // (a byte b is when its high bit and bit 7 of (b & 0x7f) + 0x13 are + // set), byte-swapped little-endian words compare in byte order. + // Otherwise the lowest set byte of their xor is the first differing + // byte. + `differ:`, + ` %dx = phi i64 [ %aw, %word ], [ %at, %tail ]`, + ` %dy = phi i64 [ %bw, %word ], [ %bt, %tail ]`, + ` %xl = and i64 %dx, 9187201950435737471`, + ` %yl = and i64 %dy, 9187201950435737471`, + ` %xa = add i64 %xl, 1374463283923456787`, + ` %ya = add i64 %yl, 1374463283923456787`, + ` %xh = and i64 %dx, %xa`, + ` %yh = and i64 %dy, %ya`, + ` %hh = or i64 %xh, %yh`, + ` %whigh = and i64 %hh, -9187201950435737472`, + ` %dlow = icmp eq i64 %whigh, 0`, + ` br i1 %dlow, label %wordorder, label %locate`, + `wordorder:`, + ` %xsw = call i64 @llvm.bswap.i64(i64 %dx)`, + ` %ysw = call i64 @llvm.bswap.i64(i64 %dy)`, + ` %wbelow = icmp ult i64 %xsw, %ysw`, + ` %wsign = select i1 %wbelow, i32 -1, i32 1`, + ` ret i32 %wsign`, + `locate:`, + ` %dxor = xor i64 %dx, %dy`, + ` %dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)`, + ` %dshift = and i64 %dtz, 56`, + ` %dxs = lshr i64 %dx, %dshift`, + ` %dys = lshr i64 %dy, %dshift`, + ` %dxb = trunc i64 %dxs to i8`, + ` %dyb = trunc i64 %dys to i8`, + ` br label %bytes`, + // The first differing bytes. + `bytes:`, + ` %bx = phi i8 [ %x, %first ], [ %dxb, %locate ]`, + ` %by = phi i8 [ %y, %first ], [ %dyb, %locate ]`, + ` %bmin = call i8 @llvm.umin.i8(i8 %bx, i8 %by)`, + ` %blow = icmp ult i8 %bmin, -19`, + ` br i1 %blow, label %byte, label %high`, + // Both at least 0xED. A lone surrogate (0xED) takes the runtime; + // supplementary lead bytes (0xF0..0xF4) order below 0xEE and 0xEF + // once 0x10 is subtracted from them. + `high:`, + ` %xed = icmp eq i8 %bx, -19`, + ` %yed = icmp eq i8 %by, -19`, + ` %ed = or i1 %xed, %yed`, + ` br i1 %ed, label %order, label %planes`, + `planes:`, + ` %xsupp = icmp uge i8 %bx, -16`, + ` %ysupp = icmp uge i8 %by, -16`, + ` %xlow = add i8 %bx, -16`, + ` %ylow = add i8 %by, -16`, + ` %xk = select i1 %xsupp, i8 %xlow, i8 %bx`, + ` %yk = select i1 %ysupp, i8 %ylow, i8 %by`, + ` %kbelow = icmp ult i8 %xk, %yk`, + ` %ksign = select i1 %kbelow, i32 -1, i32 1`, + ` ret i32 %ksign`, `byte:`, - ` %below = icmp ult i8 %x, %y`, + ` %below = icmp ult i8 %bx, %by`, ` %sign = select i1 %below, i32 -1, i32 1`, ` ret i32 %sign`, `order:`, - ` %c = call i32 @${fn}(ptr %a, ptr %b)`, + ` %acw.p = getelementptr inbounds %ScrStr, ptr %a, i32 0, i32 2`, + ` %bcw.p = getelementptr inbounds %ScrStr, ptr %b, i32 0, i32 2`, + ` %acw = load i32, ptr %acw.p`, + ` %bcw = load i32, ptr %bcw.p`, + ` %cws = or i32 %acw, %bcw`, + ` %anyascii = icmp slt i32 %cws, 0`, + ` br i1 %anyascii, label %memorder, label %units`, + `memorder:`, + ` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`, + ` ret i32 %m`, + `units:`, + ` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`, ` ret i32 %c`, `equal:`, ` ret i32 0`, @@ -4636,6 +4695,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)`); @@ -4668,6 +4730,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)})`, @@ -4945,8 +5013,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); @@ -4960,23 +5028,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(); @@ -4984,10 +5098,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); } } } @@ -5012,7 +5126,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 } } } @@ -5022,7 +5136,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]!); } @@ -5095,7 +5209,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; @@ -5625,7 +5739,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; @@ -5949,7 +6063,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 @@ -6060,7 +6174,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); } @@ -6191,11 +6305,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); @@ -6203,7 +6317,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); } @@ -6369,7 +6483,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); @@ -6515,7 +6629,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, ); } @@ -7540,14 +7654,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); @@ -7708,7 +7822,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}`); @@ -7742,7 +7856,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) { @@ -8087,7 +8202,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, @@ -8134,7 +8249,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; @@ -8161,7 +8276,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 { @@ -8219,7 +8334,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)) { @@ -8242,10 +8358,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; @@ -8272,7 +8388,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); @@ -8296,14 +8412,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); @@ -8354,7 +8472,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, @@ -8419,7 +8537,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); @@ -8569,7 +8687,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 @@ -8635,12 +8753,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); } @@ -8670,12 +8788,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); } @@ -8686,7 +8804,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); @@ -9329,7 +9447,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 @@ -9398,7 +9517,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; @@ -9444,7 +9563,7 @@ export class LlEmitter { s.kind !== "rethrow" && s.kind !== "runtimeFence" ) { - this.releaseFrame(frame); + this.releaseTemps(frame); } } @@ -9656,7 +9775,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); @@ -9697,7 +9816,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; } @@ -9709,11 +9828,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; } @@ -9867,12 +9986,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( @@ -9887,11 +10007,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}`); } @@ -9954,7 +10078,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) { @@ -10399,7 +10523,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 }); } @@ -10652,7 +10776,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); @@ -10748,7 +10872,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 }; } @@ -10787,7 +10911,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, @@ -10810,7 +10934,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 @@ -10835,7 +10959,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. @@ -10851,7 +10975,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..1cab02a9d 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); @@ -599,7 +602,7 @@ export function emitStringExpr( } case "strCmp": { const [l, r] = emitStringInputs(host, [e.left, e.right]); - const fn = e.utf16 === true ? "scr_str_cmp_u16" : "scr_str_cmp"; + const fn = "scr_str_cmp_u16"; host.declare(`declare i32 @${fn}(ptr, ptr)`); const c = B.tmp(); const t = B.tmp(); @@ -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 f21ded106..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). @@ -826,6 +832,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "util": case "bigint": case "crypto": + case "webcrypto": case "buffer": case "bytes": case "tlsca": 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 ef83bf07e..7bbb97814 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -94,6 +94,8 @@ export const LIB_FN_SYMS: Record = { "num.parseInt": "scr_parse_int", "num.parseFloat": "scr_parse_float", "str.localeCompare": "scr_str_locale_compare", + "str.substr": "scr_str_substr", + "str.localeCase": "scr_str_locale_case", "str.replaceLiteral": "scr_str_replace_literal", "str.replaceChars": "scr_str_replace_chars", "num.fromString": "scr_string_to_number", @@ -143,6 +145,8 @@ export const LIB_FN_SYMS: Record = { "num.toExponential": "scr_num_to_exponential", "num.toFixed0": "scr_num_to_fixed0", "num.toFixed": "scr_num_to_fixed", + "num.toPrecision": "scr_num_to_precision", + "num.toExponentialDigits": "scr_num_to_exponential_digits", "num.toStringRadix": "scr_num_to_string_radix", "num.toStringRadixF64": "scr_num_to_string_radix_f64", "num.sameValue": "scr_num_same_value", @@ -195,6 +199,7 @@ export const LIB_FN_SYMS: Record = { "os.cpusModel": "scr_os_cpus_model", "os.cpusField": "scr_os_cpus_field", "os.constants": "scr_os_constants", + "fs.constants": "scr_fs_constants_object", "constants.object": "scr_node_constants", "os.release": "scr_os_release", "os.userName": "scr_os_user_name", @@ -326,6 +331,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. @@ -405,6 +413,7 @@ export const LIB_FN_SYMS: Record = { "buffer.fromStr": "scr_bytes_from_str", "buffer.fromStrChecked": "scr_bytes_from_str_checked", "buffer.brand": "scr_bytes_as_buffer", + "buffer.isBranded": "scr_bytes_is_buffer", "buffer.fromDyn": "scr_buffer_from_dyn", "buffer.concat": "scr_buffer_concat", "buffer.concatLen": "scr_buffer_concat_len", @@ -571,12 +580,19 @@ export const LIB_FN_SYMS: Record = { "crypto.randomIntMax": "scr_crypto_random_int_max", "crypto.randomFillView": "scr_crypto_random_fill", "crypto.randomFillViewRest": "scr_crypto_random_fill_rest", + "crypto.getRandomValues": "scr_crypto_get_random_values", + "crypto.getRandomValuesDyn": "scr_crypto_get_random_values_dyn", "crypto.randomFillSyncDyn": "scr_crypto_random_fill_sync_dyn", "crypto.randomUUIDOpts": "scr_crypto_random_uuid_opts", "crypto.createSecretKey": "scr_crypto_create_secret_key", "crypto.generateKeySync": "scr_crypto_generate_key_sync", "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", + "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.sign": "scr_webcrypto_sign", + "webcrypto.verify": "scr_webcrypto_verify", + "webcrypto.digest": "scr_webcrypto_digest", + "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -883,12 +899,16 @@ export const LIB_FN_SYMS: Record = { "dyn.typedRefIs": "scr_dyn_typed_ref_is_key", "dyn.classIs": "scr_dyn_class_is_key", "dyn.iterPack": "scr_dyn_iter_pack", + "dyn.destructureRest": "scr_dyn_destructure_rest", "dyn.iterator": "scr_dyn_iterator", "dyn.arrayFromIterator": "scr_dyn_array_from_iterator", "dyn.iteratorResult": "scr_dyn_iterator_result", "dyn.iteratorCanStep": "scr_dyn_iterator_can_step", "dyn.iteratorStep": "scr_dyn_iterator_step", "dyn.iteratorStepDone": "scr_dyn_iterator_step_done", + "dyn.setGeneratorMembers": "scr_dyn_set_generator_members", + "dyn.iteratorHelperMethod": "scr_dyn_iterator_helper_method", + "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", @@ -1409,6 +1429,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. @@ -1455,6 +1476,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", // Decipheriv.setAuthTag reports DEP0182 on the next tick. "crypto.cipherNew", + // WebCrypto results settle through the deferred crypto queue. + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", "process.emitWarningText", // The legacy url entries queue DEP0169/DEP0170 on the next tick. "url.legacyParse", 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 9397b7aee..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", 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 cbbf0cc29..c7a58a7ea 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -236,7 +236,7 @@ function emitRecordCloneFields( const fieldTy = llFieldType(from.type, from.inline); const v = B.tmp(); B.line(`${v} = load ${fieldTy}, ptr ${from.ptr}`); - const stored = isRefCounted(from.type) && !from.inline ? host.retainValue(v, from.type) : v; + const stored = isRefCounted(from.type) && !from.inline ? host.retainFrame(v, from.type) : v; B.line(`store ${fieldTy} ${stored}, ptr ${to.ptr}`); } const initialized = new Set(); diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index bf140d2e6..8898b86d3 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -432,7 +432,6 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", op: "<", left: str("a"), right: str("b"), - utf16: true, type: BOOL, loc, }); @@ -443,16 +442,14 @@ test("borrowed literals keep UTF-16 string comparison without temporary owners", }); test("string comparisons decide identity, lengths and leading bytes before the runtime", () => { - const order = (utf16: boolean) => - fn(`order${utf16 ? 16 : 8}`, [], { - kind: "strCmp", - op: "<", - left: str("apple"), - right: str("apricot"), - utf16, - type: BOOL, - loc, - }); + const order = fn("order", [], { + kind: "strCmp", + op: "<", + left: str("apple"), + right: str("apricot"), + type: BOOL, + loc, + }); const same = fn("same", ["a", "b"], { kind: "strEq", left: { kind: "varRef", localId: "a", type: STRING, loc }, @@ -462,23 +459,28 @@ test("string comparisons decide identity, lengths and leading bytes before the r loc, }); for (const bits of [32, 64] as const) { - const ir = emitLlvmModule(mod(order(false), order(true), same), { pointerBits: bits }); + const ir = emitLlvmModule(mod(order, same), { pointerBits: bits }); const helper = (name: string) => new RegExp(`^define internal [^\\n]*@${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(ir)![0]; const eq = helper("sc_str_eq"); expect(eq).toContain("icmp eq ptr %a, %b"); expect(eq).toMatch(/icmp eq i(32|64) %alen, %blen/); expect(eq).toContain("call zeroext i1 @scr_str_eq(ptr %a, ptr %b)"); - const bytes = helper("sc_str_cmp"); - expect(bytes).toContain("@llvm.bswap.i64"); - expect(bytes).not.toContain("%ascii"); - expect(bytes).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); const units = helper("sc_str_cmp_u16"); - expect(units).toContain("%wascii = icmp eq i64 %whigh, 0"); - expect(units).toContain("%tascii = icmp eq i64 %thigh, 0"); - expect(bytes).toContain("%tailable = and i1 %samelen, %short"); - expect(units).toContain("%ascii = icmp sge i8 %bits, 0"); - expect(units).toContain("call i32 @scr_str_cmp_u16(ptr %a, ptr %b)"); + expect(units).toContain("%dx = phi i64 [ %aw, %word ], [ %at, %tail ]"); + expect(units).toContain("%dlow = icmp eq i64 %whigh, 0"); + expect(units).toContain("@llvm.bswap.i64"); + expect(units).toContain("%dtz = call i64 @llvm.cttz.i64(i64 %dxor, i1 true)"); + expect(units).toContain("%bx = phi i8 [ %x, %first ], [ %dxb, %locate ]"); + expect(units).toContain("%blow = icmp ult i8 %bmin, -19"); + expect(units).toContain("%kbelow = icmp ult i8 %xk, %yk"); + expect(units).toContain("%below = icmp ult i8 %bx, %by"); + expect(units).toContain("%tailable = and i1 %samelen, %short"); + expect(units).toContain("%anyascii = icmp slt i32 %cws, 0"); + expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)"); + expect(units).toContain( + "call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)", + ); } }); diff --git a/packages/compiler/src/backend/llvm/string-slices.ts b/packages/compiler/src/backend/llvm/string-slices.ts index ae73f1ea1..d00c681d6 100644 --- a/packages/compiler/src/backend/llvm/string-slices.ts +++ b/packages/compiler/src/backend/llvm/string-slices.ts @@ -91,7 +91,7 @@ export function emitStringSliceSnapshot( B.line( `call void @scr_str_slice_range(ptr ${source.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${range})`, ); - host.moveTemp(source); + host.toFrame(source); return { source: sourceSlot, range }; } diff --git a/packages/compiler/src/backend/llvm/thread-slots.ts b/packages/compiler/src/backend/llvm/thread-slots.ts index da182bde2..85656878a 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.ts @@ -49,7 +49,7 @@ export function emitThreadSlotsIntrinsic( case "slots.get": { const read = readSlot(host, block, word); if (host.nullableUnions.of(e.type) !== null) - return host.own({ name: host.retainValue(read, e.type), type: e.type }); + return host.own({ name: host.retainFrame(read, e.type), type: e.type }); return boxedRead(host, read, e.type); } case "slots.has": { @@ -67,13 +67,13 @@ export function emitThreadSlotsIntrinsic( case "slots.set": { if (value === null) throw new InternalCompilerError("llvm emitter bug: slots.set value"); // The value moves into the block. - host.moveTemp(value); + host.toHeap(value); host.declare(`declare void @scr_slots_set(ptr, ptr, ptr, ptr, ptr)`); B.line( `call void @scr_slots_set(ptr ${block}, ptr ${key.name}, ptr ${word}, ptr ${value.name}, ptr ${releaseSym(host.shapeHost, value.type)})`, ); if (e.type.kind === "void") return { name: "", type: e.type }; - return host.own({ name: host.retainValue(slots.name, e.type), type: e.type }); + return host.own({ name: host.retainFrame(slots.name, e.type), type: e.type }); } default: throw new InternalCompilerError(`llvm emitter bug: unknown intrinsic ${e.name}`); @@ -100,7 +100,7 @@ function boxedRead(host: LlvmEmitterContext, read: string, type: IrType): LlValu B.condBr(has, some, none); B.startBlock(some); const arm = def.arms[refTag]!; - const box = host.unionNewOwned(def.id, refTag, { name: host.retainValue(read, arm), type: arm }); + const box = host.unionNewOwned(def.id, refTag, { name: host.retainHeap(read, arm), type: arm }); B.line(`store ptr ${box}, ptr ${result}`); B.br(done); B.startBlock(none); 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 8f69cea57..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; @@ -48,6 +51,9 @@ export interface NativeLinkFeatures { * archive. */ cipher: boolean; tlsCa: boolean; + /** crypto.subtle importKey/sign/verify/digest and CryptoKey values: + * scr_webcrypto.c. */ + webcrypto: boolean; } export interface NativeLinkInfo { diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 5e981fe44..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 @@ -197,6 +199,8 @@ export interface CcOptions { /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links * the mbedTLS archive (shared with tls). */ cipher?: boolean; + /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ + webcrypto?: boolean; /** Internal compiler hook: called only after a strict native artifact hit * or a successful stable build has installed `outPath`. The executable * frontend cache uses it to publish its stamp after native dependencies diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 549e9ebbf..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 @@ -577,6 +578,7 @@ export async function compileCInternal( ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []), ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []), ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []), + ...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []), @@ -643,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/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/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index faa335e33..fd57cb49d 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -119,6 +119,7 @@ const features = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test.each([ diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ffe581366..61d1dba5e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 67e627c91..2cd111060 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, }); 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/coverage/verdict-cache.test.ts b/packages/compiler/src/coverage/verdict-cache.test.ts index 1c3e9bc3d..0380e8b8d 100644 --- a/packages/compiler/src/coverage/verdict-cache.test.ts +++ b/packages/compiler/src/coverage/verdict-cache.test.ts @@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true })); test("unchanged and comment-only reruns replay the verdict; code edits relower", () => { const entry = join(dir, "main.ts"); - const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`; + // The blocker: an object pattern over a union source (arrays alone compile). + const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`; writeFileSync(entry, program); const entries = new Map(); const store: VerdictStore = { diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index e5887e855..1b42c3eca 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(): Promise<{ diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index fbd7e4edd..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; @@ -79,6 +80,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; tlsCa: boolean; + webcrypto: boolean; } interface EarlyExecutableCacheStamp { @@ -111,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. */ @@ -190,6 +194,7 @@ const BOOLEAN_NATIVE_KEYS = [ "tls", "tlsCa", "cipher", + "webcrypto", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures { @@ -200,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"), ); } @@ -221,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" @@ -260,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..c30788fa1 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -109,7 +109,7 @@ function markerNamed( if (!MARKERS.has(written)) return null; let member = written; const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; let importDecl: ts.Node | undefined; if (ts.isImportSpecifier(decl) && !qualified) { @@ -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 86aa10933..8fc2238bf 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -18,7 +18,7 @@ export function experimentalImportOf( ): ExperimentalMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( @@ -67,7 +67,7 @@ export function lifetimeRegionMemberOf( ): "run" | "release" | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( @@ -91,7 +91,7 @@ export function internTableMemberOf( ): InternTableMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if ( decl === undefined || !( @@ -129,7 +129,7 @@ export function threadSlotsMemberOf( ): ThreadSlotsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(access.name); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isMethodDeclaration(decl)) return null; const owner = decl.parent; if ( diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 0562467be..592bbb2d6 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -8,7 +8,7 @@ import { moduleSourceCandidates } from "./module-source-candidates.js"; export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(expression); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; return ( declarations.length > 0 && declarations.every((declaration) => { @@ -45,7 +45,7 @@ export function isBuiltinMemberImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl !== undefined && ts.isImportSpecifier(decl)) { const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -60,7 +60,7 @@ export function isBuiltinMemberImport( } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let parent: ts.Node | undefined = targetDecl.parent; @@ -88,7 +88,7 @@ export function isBuiltinNamespaceImport( ): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; let importDecl: ts.ImportDeclaration | undefined; if (decl !== undefined && ts.isNamespaceImport(decl)) { const candidate = decl.parent?.parent; @@ -108,12 +108,9 @@ function constInitializer(program: ts.Program, expr: ts.Expression): ts.Expressi if (!ts.isIdentifier(current)) return null; const symbol = program.getTypeChecker().getSymbolAtLocation(current); const decl = symbol - ? program - .getTypeChecker() - .declarationsOf(symbol) - .find((candidate): candidate is ts.VariableDeclaration => - ts.isVariableDeclaration(candidate), - ) + ? symbol.declarations.find((candidate): candidate is ts.VariableDeclaration => + ts.isVariableDeclaration(candidate), + ) : undefined; if ( decl === undefined || @@ -158,13 +155,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 +191,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 +216,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 +234,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 +258,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 +276,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 +288,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 +301,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..7095cea5d 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -25,15 +25,13 @@ export function inferredJsDiagnosticSuppressed( const symbol = checker.getSymbolAtLocation(node.name); if ( symbol && - checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.type && - !decl.initializer && - !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.type && + !decl.initializer && + !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))), + ) ) { // Bare JS fields exist with value undefined before the first write; // native storage includes that arm even when inference omits it. @@ -43,16 +41,17 @@ export function inferredJsDiagnosticSuppressed( } if (diagnostic.code === 2554 && ts.isCallExpression(node)) { const signature = checker.getResolvedSignature(node); - const declaration = signature && checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( declaration && isJsSourceFileName(declaration.getSourceFile().fileName) && (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; @@ -82,9 +81,7 @@ export function inferredJsDiagnosticSuppressed( checker.getTypeOfSymbolAtLocation(throwMethod, node.expression), ); const declaration = - throwSignatures.length === 1 - ? checker.signatureDeclaration(throwSignatures[0]!) - : undefined; + throwSignatures.length === 1 ? throwSignatures[0]!.declaration : undefined; // A sole throw cannot return an invalid IteratorResult. Avoid inferring // termination through arbitrary control flow or explicit return types. if ( @@ -92,8 +89,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..ee8a34ea2 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; @@ -176,7 +176,7 @@ export function assertInstanceOf( ): AssertInstanceOptions | null { if (!ts.isIdentifier(expr)) return null; const symbol = lowerer.checker.getSymbolAtLocation(expr); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; return staticAssertInstanceDecl(lowerer, decl.name, decl.initializer); } diff --git a/packages/compiler/src/frontend/lowering/assertion-identity.ts b/packages/compiler/src/frontend/lowering/assertion-identity.ts new file mode 100644 index 000000000..6a00b7afe --- /dev/null +++ b/packages/compiler/src/frontend/lowering/assertion-identity.ts @@ -0,0 +1,78 @@ +/* Type assertions of the global object. + * + * `globalThis as { [KEY]?: State }` is the process-global state idiom: a + * module asserts the global object to a record type and reads and writes + * its members, and every other module that asserts it sees the same + * members. Node evaluates the assertion to the global object itself. + * + * The global object is a checked-dynamic value, and a checked-dynamic value + * asserted to a record or array type takes the checked extraction + * (dynCheck), which builds a new native value. For the global object that + * copy is always wrong: a write through it never reached the global object, + * and a view bound before another module's write never saw it. So an + * assertion of the global object keeps the checked-dynamic value itself: a + * view whose member reads and writes go to the global object, and bindings + * of it hold that object. + * + * Other checked-dynamic operands keep the checked extraction: a native value + * held as `unknown` unwraps by identity there, and a checked-dynamic tree is + * copied at the documented checked-value boundary. + */ +import * as ts from "../ts7/adapter.js"; +import type { IrType } from "../../ir/ir.js"; +import type { Lowerer } from "./lowerer.js"; +import { stdlibGlobalNameOf } from "./surfaces.js"; + +/** A target whose checked extraction builds a new mutable value. */ +export function extractionCopiesReference(lowerer: Lowerer, type: IrType): boolean { + const seen = new Set(); + const copies = (t: IrType): boolean => { + if (t.kind === "record" || t.kind === "array") return true; + if (t.kind !== "union" || seen.has(t.unionId)) return false; + seen.add(t.unionId); + return lowerer.unions.get(t.unionId)?.arms.some(copies) ?? false; + }; + return copies(type); +} + +/** The asserted expression without parentheses, `!` and nested + * assertions (`globalThis as unknown as T`). */ +function assertedSource(expr: ts.Expression): ts.Expression { + let e = expr; + while ( + ts.isParenthesizedExpression(e) || + ts.isNonNullExpression(e) || + ts.isAsExpression(e) || + ts.isTypeAssertion(e) || + ts.isSatisfiesExpression(e) + ) + e = e.expression; + return e; +} + +/** `expr` asserts the global object (`globalThis`, `global`). */ +export function assertsGlobalObject( + lowerer: Lowerer, + expr: ts.AsExpression | ts.TypeAssertion, +): boolean { + const name = stdlibGlobalNameOf(lowerer, assertedSource(expr.expression)); + return name === "globalThis" || name === "global"; +} + +/** The assertion a binding initializer is, when it keeps the global object: + * bindings of it hold that object rather than a native value of the + * asserted type. A target with no native layout keeps the object as well + * (the assertion lowers to the checked-dynamic value either way). */ +export function keptIdentityAssertion( + lowerer: Lowerer, + initializer: ts.Expression, +): ts.AsExpression | ts.TypeAssertion | undefined { + let e = initializer; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isAsExpression(e) && !ts.isTypeAssertion(e)) return undefined; + if (!assertsGlobalObject(lowerer, e)) return undefined; + const target = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(e.type)); + return !target || target.kind === "dyn" || extractionCopiesReference(lowerer, target) + ? e + : undefined; +} diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 816294e08..88b0f91dc 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -114,7 +114,7 @@ export function nullishValueUnitOf( const cached = lowerer.nullishBindings.get(sym); if (cached !== undefined) return cached; lowerer.nullishBindings.set(sym, null); // cycle guard: self-referential probes answer non-qualifying - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; // Statement-position declarators only: a for-loop head (`for (let x = // null as any; ...)`) declares a LOCAL with per-iteration semantics — // lowerVarDeclList's contract requires a lowered statement for it, so 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..e6de99b09 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -34,7 +34,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression // has no other first-class story. if (ts.isIdentifier(node) && node.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(node); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { return { kind: "libCall", @@ -106,14 +106,14 @@ export function lowerDiagnosticsChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "publish" && call.arguments.length === 1) { + if (name === "publish" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const msg = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const msg = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.publish", args: [receiver, msg], type: VOID, loc }; } - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.chanSubscribe", args: [receiver, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.chanUnsubscribe", args: [receiver, cb], type: BOOL, loc }; @@ -126,18 +126,18 @@ export function lowerDiagnosticsChannelMethodCall( // this/arguments to fn exactly like the trace calls. if ( (name === "bindStore" || name === "unbindStore") && - call.arguments.length >= 1 && - call.arguments.length <= (name === "bindStore" ? 2 : 1) + call.arguments.nodes.length >= 1 && + call.arguments.nodes.length <= (name === "bindStore" ? 2 : 1) ) { - if (!isBuiltinClassInstance(lowerer, call.arguments[0]!, "AsyncLocalStorage")) { + if (!isBuiltinClassInstance(lowerer, call.arguments.nodes[0]!, "AsyncLocalStorage")) { lowerer.noLowering( `Channel.${name} with this store argument`, - call.arguments[0]!, + call.arguments.nodes[0]!, "an AsyncLocalStorage instance (node:async_hooks) is the supported store", ); } const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const store = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const store = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); if (name === "unbindStore") { return { kind: "libCall", @@ -148,8 +148,8 @@ export function lowerDiagnosticsChannelMethodCall( }; } const transform: IrExpr = - call.arguments[1] !== undefined - ? lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!) + call.arguments.nodes[1] !== undefined + ? lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!) : dynUndefinedExpr(loc); return { kind: "libCall", @@ -159,15 +159,15 @@ export function lowerDiagnosticsChannelMethodCall( loc, }; } - if (name === "runStores" && call.arguments.length >= 2) { + if (name === "runStores" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const data = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); + const data = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); const thisArg: IrExpr = - call.arguments[2] !== undefined - ? lowerDiagnosticsMessage(lowerer, call.arguments[2]!) + call.arguments.nodes[2] !== undefined + ? lowerDiagnosticsMessage(lowerer, call.arguments.nodes[2]!) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3), loc); return { kind: "libCall", fn: "dc.chanRunStores", @@ -198,7 +198,7 @@ export function lowerAsyncLocalStorageMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "getStore" && call.arguments.length === 0) { + if (name === "getStore" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", @@ -208,11 +208,11 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "run" && call.arguments.length >= 2) { + if (name === "run" && call.arguments.nodes.length >= 2) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[1]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(2), loc); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[1]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(2), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.run", @@ -221,10 +221,10 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "exit" && call.arguments.length >= 1) { + if (name === "exit" && call.arguments.nodes.length >= 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(1), loc); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(1), loc); return asyncLocalStorageResult(lowerer, call, { kind: "libCall", fn: "als.exitRun", @@ -233,12 +233,12 @@ export function lowerAsyncLocalStorageMethodCall( loc, }); } - if (name === "enterWith" && call.arguments.length === 1) { + if (name === "enterWith" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const value = lowerDiagnosticsMessage(lowerer, call.arguments[0]!); + const value = lowerDiagnosticsMessage(lowerer, call.arguments.nodes[0]!); return { kind: "libCall", fn: "als.enterWith", args: [receiver, value], type: VOID, loc }; } - if (name === "disable" && call.arguments.length === 0) { + if (name === "disable" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); return { kind: "libCall", fn: "als.disable", args: [receiver], type: VOID, loc }; } @@ -301,13 +301,13 @@ function lowerTracingHandlers(lowerer: Lowerer, node: ts.Expression): IrExpr { if ( ts.isObjectLiteralExpression(e) && lowerer.mapTypeOf(lowerer.typeOf(e))?.kind === "record" && - e.properties.length > 0 && - e.properties.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) + e.properties.nodes.length > 0 && + e.properties.nodes.every((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) ) { const loc = locOf(e); return { kind: "dynObjLit", - fields: e.properties.map((p) => { + fields: e.properties.nodes.map((p) => { const pa = p as ts.PropertyAssignment; return { key: { @@ -366,32 +366,32 @@ export function lowerTracingChannelMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if ((name === "subscribe" || name === "unsubscribe") && call.arguments.length === 1) { + if ((name === "subscribe" || name === "unsubscribe") && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const handlers = lowerTracingHandlers(lowerer, call.arguments[0]!); + const handlers = lowerTracingHandlers(lowerer, call.arguments.nodes[0]!); return name === "subscribe" ? { kind: "libCall", fn: "dc.tcSubscribe", args: [receiver, handlers], type: VOID, loc } : { kind: "libCall", fn: "dc.tcUnsubscribe", args: [receiver, handlers], type: BOOL, loc }; } if ( (name === "traceSync" || name === "traceCallback" || name === "tracePromise") && - call.arguments.length >= 1 + call.arguments.nodes.length >= 1 ) { const receiver = lowerer.lowerExprExpecting(access.expression, F64); - const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments[0]!); + const fn = lowerDiagnosticsSubscriber(lowerer, call.arguments.nodes[0]!); // traceSync(fn, context?, thisArg?, ...args) / // tracePromise(fn, context?, thisArg?, ...args) / // traceCallback(fn, position?, context?, thisArg?, ...args) const shift = name === "traceCallback" ? 1 : 0; - const ctxNode = call.arguments[1 + shift]; - const thisNode = call.arguments[2 + shift]; + const ctxNode = call.arguments.nodes[1 + shift]; + const thisNode = call.arguments.nodes[2 + shift]; const ctx: IrExpr = ctxNode !== undefined ? lowerDiagnosticsMessage(lowerer, ctxNode) : { kind: "dynObjLit", fields: [], type: DYN, loc }; const thisArg: IrExpr = thisNode !== undefined ? lowerDiagnosticsMessage(lowerer, thisNode) : dynUndefinedExpr(loc); - const rest = lowerTracingArguments(lowerer, call.arguments.slice(3 + shift), loc); + const rest = lowerTracingArguments(lowerer, call.arguments.nodes.slice(3 + shift), loc); if (name === "traceSync") { return { kind: "libCall", @@ -416,8 +416,8 @@ export function lowerTracingChannelMethodCall( }; } const pos: IrExpr = - call.arguments[1] !== undefined - ? lowerer.lowerExprExpecting(call.arguments[1]!, F64) + call.arguments.nodes[1] !== undefined + ? lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64) : { kind: "numLit", value: -1, type: F64, loc }; return { kind: "libCall", @@ -473,29 +473,32 @@ export function lowerDiagnosticsChannelCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - if (bi.member === "channel" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "channel" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.channel", args: [name], type: F64, loc }; } - if ((bi.member === "subscribe" || bi.member === "unsubscribe") && expr.arguments.length === 2) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); - const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments[1]!); + if ( + (bi.member === "subscribe" || bi.member === "unsubscribe") && + expr.arguments.nodes.length === 2 + ) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); + const cb = lowerDiagnosticsSubscriber(lowerer, expr.arguments.nodes[1]!); return bi.member === "subscribe" ? { kind: "libCall", fn: "dc.subscribe", args: [name, cb], type: VOID, loc } : { kind: "libCall", fn: "dc.unsubscribe", args: [name, cb], type: BOOL, loc }; } - if (bi.member === "hasSubscribers" && expr.arguments.length === 1) { - const name = diagnosticsChannelNameArgument(lowerer, expr.arguments[0]!); + if (bi.member === "hasSubscribers" && expr.arguments.nodes.length === 1) { + const name = diagnosticsChannelNameArgument(lowerer, expr.arguments.nodes[0]!); return { kind: "libCall", fn: "dc.hasSubscribers", args: [name], type: BOOL, loc }; } // tracingChannel: the string form interns the five tracing::* // channels; the collection form takes an object literal whose five // event members are Channel-typed values (Node's TracingChannelCollection). - if (bi.member === "tracingChannel" && expr.arguments.length === 1) { - const a = expr.arguments[0]!; + if (bi.member === "tracingChannel" && expr.arguments.nodes.length === 1) { + const a = expr.arguments.nodes[0]!; if (ts.isObjectLiteralExpression(a)) { const byEvent = new Map(); - for (const p of a.properties) { + for (const p of a.properties.nodes) { if ( !ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name) || diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index b4a394dce..41e35a511 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -45,9 +45,9 @@ export function lowerAtomicsCall( : 3; const maximum = member === "wait" ? 4 : member === "notify" ? 3 : minimum; if ( - call.arguments.length < minimum || - call.arguments.length > maximum || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < minimum || + call.arguments.nodes.length > maximum || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Atomics.${member} arguments`, @@ -55,7 +55,7 @@ export function lowerAtomicsCall( `use ${minimum}${maximum > minimum ? `–${maximum}` : ""} arguments`, ); } - const source = call.arguments[0]!; + const source = call.arguments.nodes[0]!; // Generic constraints can remain unions in the checker while this body // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); @@ -75,8 +75,8 @@ export function lowerAtomicsCall( } const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); const arg = (index: number, fallback: number): IrExpr => - call.arguments[index] - ? lowerOptionalArgument(lowerer, call.arguments[index]!, F64, number(fallback)) + call.arguments.nodes[index] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[index]!, F64, number(fallback)) : number(fallback); const index = arg(1, 0); if (member === "wait") diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts index 14d3c6cce..3fc4ec173 100644 --- a/packages/compiler/src/frontend/lowering/builtins/blob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts @@ -70,7 +70,7 @@ function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpr export function lowerBlobNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const cls = blobClassOf(lowerer, expr.expression); if (cls === null) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); const values = args.map((arg) => @@ -94,10 +94,10 @@ export function lowerStaticBlobCall(lowerer: Lowerer, call: ts.CallExpression): const member = access.name.text; if (access.questionDotToken || !BLOB_CALLS.has(member)) return null; if (blobReceiverClass(lowerer, access.expression) === null) return null; - if (call.arguments.some(ts.isSpreadElement)) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; const loc = locOf(call); const receiver = lowerer.lowerExprExpecting(access.expression, DYN); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); if (member === "slice") { return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index cf19b4003..1669978af 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -126,7 +126,7 @@ export function registerPromisifiedBuiltinDecl( if (registerHttpClientFnBinding(lowerer, nameNode, init)) return true; const e = isPromisifyCall(lowerer, init); if (!e) return false; - const argNode = e.arguments.length === 1 ? e.arguments[0]! : null; + const argNode = e.arguments.nodes.length === 1 ? e.arguments.nodes[0]! : null; // `promisify(exec)` over a named import, or `promisify(cp.exec)` over a // namespace import. const target = !argNode diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 04b3caba8..5590fbe9f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -42,9 +42,9 @@ export function lowerExecFileAsyncCall( ): IrExpr { const what = shell ? "the promisified exec" : "the promisified execFile"; if ( - expr.arguments.length < 1 || - expr.arguments.length > (shell ? 2 : 3) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > (shell ? 2 : 3) || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `${what} with this argument shape`, @@ -54,7 +54,7 @@ export function lowerExecFileAsyncCall( : "the supported form is execFileAsync(file, args?, options?)", ); } - const command = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const command = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // exec: /bin/sh -c , execSync's lowering (the command string // rides as the argv[1] the failure display reads); execFile: file + its // args list. @@ -66,8 +66,8 @@ export function lowerExecFileAsyncCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[shell ? 1 : 2]; + : lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[shell ? 1 : 2]; const emptyStr: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; const helper = execFileAsyncHelper(lowerer, loc, shell); @@ -93,7 +93,7 @@ export function lowerExecFileAsyncCall( "pass the options inline so each member can be checked", ); } - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the portless // opensslAsync shape: `...(c ? { env: { ...process.env, ...extra // } } : {})` (either orientation). The condition evaluates ONCE diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index 1fc015892..a435bf740 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -35,22 +35,22 @@ export function lowerChildMethodCall( const loc = locOf(call); if (name === "send") { if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `child.send with ${call.arguments.length} arguments`, + `child.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } const receiver = lowerer.lowerExpr(access.expression); - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "child.send", args: [receiver, json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "child.sendCb", @@ -59,7 +59,7 @@ export function lowerChildMethodCall( loc, }; } - if (name === "disconnect" && call.arguments.length === 0) { + if (name === "disconnect" && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -75,8 +75,8 @@ export function lowerChildMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if ( event !== "spawn" && @@ -88,7 +88,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( `child.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)', ); } @@ -105,7 +105,7 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onMessage", - args: [receiver, ipcMessageListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; @@ -114,24 +114,24 @@ export function lowerChildMethodCall( return { kind: "libCall", fn: "child.onDisconnect", - args: [receiver, ipcDisconnectListener(lowerer, call.arguments[1]!), once], + args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once], type: VOID, loc, }; } - const listenerNode = stripTypeCasts(call.arguments[1]!); + const listenerNode = stripTypeCasts(call.arguments.nodes[1]!); if ( isJsSourceFile(listenerNode.getSourceFile()) && (ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) && - listenerNode.parameters.some((param) => param.dotDotDotToken) && - listenerNode.parameters.every((param) => !param.type) && + listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) && + listenerNode.parameters.nodes.every((param) => !param.type) && !/@(?:param|type)\b/.test( listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()), ) ) { - for (const param of listenerNode.parameters) lowerer.checkedCallbackParams.add(param); + for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param); } - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); if ( (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") || (cb.type.kind === "func" && @@ -149,7 +149,7 @@ export function lowerChildMethodCall( ) { lowerer.noLowering( "child event listeners with non-representable parameters", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { @@ -168,7 +168,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") { lowerer.noLowering( "spawn event listeners with parameters or a returned value", - call.arguments[1]!, + call.arguments.nodes[1]!, ); } return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc }; @@ -176,7 +176,7 @@ export function lowerChildMethodCall( if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event !== "error" ? `${event} listeners with more than two parameters (use (code, signal), (code), or ())` : "error listeners with more than one parameter (use (err) or ())", @@ -188,7 +188,7 @@ export function lowerChildMethodCall( // fenced instead of being called through the wrong ABI. lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "listeners returning a value (make the callback body a block, or return nothing)", ); } @@ -206,7 +206,7 @@ export function lowerChildMethodCall( if (!armsOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose parameter is not 'number | null' (got '${lowerer.fmt(param!)}')`, ); } @@ -228,7 +228,7 @@ export function lowerChildMethodCall( if (!sigOk) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`, ); } @@ -243,7 +243,7 @@ export function lowerChildMethodCall( if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`, ); } @@ -256,11 +256,11 @@ export function lowerChildMethodCall( // reaped or never spawned (Node's null-handle answer), and a successful // send sets `killed`. if (name === "kill") { - if (call.arguments.length > 1) { - lowerer.noLowering(`child.kill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`child.kill with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const sigNode = call.arguments[0]; + const sigNode = call.arguments.nodes[0]; if (!sigNode) { const dflt: IrExpr = { kind: "strLit", value: "SIGTERM", type: STRING, loc }; return { kind: "libCall", fn: "child.kill", args: [receiver, dflt], type: BOOL, loc }; @@ -281,7 +281,7 @@ export function lowerChildMethodCall( // child.unref(): drops the child from the event loop's keep-alive set // (the process may exit while the child runs — Node's semantics; the // child is still reaped while the loop runs for other reasons). - if ((name === "unref" || name === "ref") && call.arguments.length === 0) { + if ((name === "unref" || name === "ref") && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", @@ -320,9 +320,9 @@ export function lowerChildStreamMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "setEncoding" && call.arguments.length === 1) { + if (name === "setEncoding" && call.arguments.nodes.length === 1) { const receiver = lowerer.lowerExprExpecting(access.expression, CHILDSTREAM_T); - const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments[0]!); + const enc = bufEncoding(lowerer, "child stream setEncoding", call.arguments.nodes[0]!); return { kind: "libCall", fn: "stream.childSetEncoding", @@ -331,13 +331,13 @@ export function lowerChildStreamMethodCall( loc, }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const evT = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const evT = lowerer.typeOf(call.arguments.nodes[0]!); const event = evT.isStringLiteralType() ? evT.value : null; if (event !== "data" && event !== "end") { lowerer.noLowering( `stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"data" and "end" are the supported child-stream events (as literals)', ); } @@ -349,7 +349,7 @@ export function lowerChildStreamMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc }; if ( cb.type.kind !== "func" || @@ -358,7 +358,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "data" ? "data listeners with more than one parameter or a return value (use (chunk) or ())" : "end listeners with parameters or a return value (use ())", @@ -381,7 +381,7 @@ export function lowerChildStreamMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`, ); } @@ -417,15 +417,15 @@ export function lowerChildWriterMethodCall( const loc = locOf(call); if (name === "write") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `child stdin write with ${call.arguments.length} arguments`, + `child stdin write with ${call.arguments.nodes.length} arguments`, call, "write(string | Uint8Array) with no encoding or callback is supported", ); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const data = lowerer.lowerExpr(call.arguments[0]!); + const data = lowerer.lowerExpr(call.arguments.nodes[0]!); if (data.type.kind === "string") { return { kind: "libCall", fn: "writer.writeString", args: [receiver, data], type: BOOL, loc }; } @@ -434,12 +434,12 @@ export function lowerChildWriterMethodCall( } lowerer.noLowering( `child stdin write of '${lowerer.fmt(data.type)}'`, - call.arguments[0]!, + call.arguments.nodes[0]!, "write one string, Buffer, or Uint8Array value (narrow unions first)", ); } - if ((name === "end" || name === "destroy") && call.arguments.length === 0) { + if ((name === "end" || name === "destroy") && call.arguments.nodes.length === 0) { if (!ts.isExpressionStatement(call.parent)) { lowerer.unsupported( "SC1090", @@ -457,13 +457,13 @@ export function lowerChildWriterMethodCall( }; } - if ((name === "on" || name === "once") && call.arguments.length === 2) { - const eventType = lowerer.typeOf(call.arguments[0]!); + if ((name === "on" || name === "once") && call.arguments.nodes.length === 2) { + const eventType = lowerer.typeOf(call.arguments.nodes[0]!); const event = eventType.isStringLiteralType() ? eventType.value : null; if (event !== "drain" && event !== "finish" && event !== "error") { lowerer.noLowering( `child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`, - call.arguments[0]!, + call.arguments.nodes[0]!, '"drain", "finish", and "error" are the supported child stdin events', ); } @@ -471,7 +471,7 @@ export function lowerChildWriterMethodCall( lowerer.unsupported("SC1090", call, "chaining child stdin listener registration"); } const receiver = lowerer.lowerExprExpecting(access.expression, CHILDWRITER_T); - const cb = lowerer.lowerExpr(call.arguments[1]!); + const cb = lowerer.lowerExpr(call.arguments.nodes[1]!); const maxParams = event === "error" ? 1 : 0; if ( cb.type.kind !== "func" || @@ -480,7 +480,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, event === "error" ? "error listeners with at most one Error parameter and no return value" : `${event} listeners with no parameters or return value`, @@ -493,7 +493,7 @@ export function lowerChildWriterMethodCall( ) { lowerer.unsupported( "SC1090", - call.arguments[1]!, + call.arguments.nodes[1]!, "child stdin error listeners whose parameter is Error", ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index bfa2bc99a..24adcc1ba 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -48,15 +48,15 @@ export function lowerExecSyncCall( shell: boolean, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${shell ? "execSync" : "execFileSync"} with a spread call`, expr); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); // execSync: /bin/sh -c ; execFileSync: file + its args list. let argvExpr: IrExpr; let optsNode: ts.Expression | undefined; if (shell) { - if (expr.arguments.length > 2) { + if (expr.arguments.nodes.length > 2) { lowerer.noLowering( "execSync with this argument shape", expr, @@ -69,17 +69,17 @@ export function lowerExecSyncCall( type: arrayOf(STRING), loc, }; - optsNode = expr.arguments[1]; + optsNode = expr.arguments.nodes[1]; } else { - if (expr.arguments.length > 3) { + if (expr.arguments.nodes.length > 3) { lowerer.noLowering( "execFileSync with this argument shape", expr, "the supported form is execFileSync(file, args?, options?)", ); } - argvExpr = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - optsNode = expr.arguments[2]; + argvExpr = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + optsNode = expr.arguments.nodes[2]; } // The shell command itself is /bin/sh; the "command" string rides as // the argv[1] the display formatter reads. @@ -186,8 +186,8 @@ export function lowerExecSyncCall( modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering( @@ -206,7 +206,7 @@ export function lowerExecSyncCall( ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom carrying `env` — the openssl-runner // shape: `...(c ? { env: { ...process.env, ...extra } } : {})` // (either orientation). The condition evaluates ONCE and picks diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts index ecec14fc5..ea2dd379d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts @@ -31,8 +31,8 @@ export function lowerChildArgsArg( loc: SrcLoc, ): IrExpr { if (!node) return { kind: "arrayLit", elems: [], type: arrayOf(STRING), loc }; - if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) { - const elems = node.elements.map((el) => lowerer.lowerExprExpecting(el, STRING)); + if (ts.isArrayLiteralExpression(node) && !node.elements.nodes.some(ts.isSpreadElement)) { + const elems = node.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, STRING)); return { kind: "arrayLit", elems, type: arrayOf(STRING), loc: locOf(node) }; } return lowerer.lowerExprExpecting(node, arrayOf(STRING)); @@ -92,16 +92,16 @@ const NODE_SIGNAL_NAMES = new Set([ * fences by name. The bare `{ encoding: "utf8" }` shape keeps its * historical cp.spawnSync lowering. */ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawnSync with this argument shape", expr, "the supported form is spawnSync(command, args?, options?)", ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const argv = lowerChildArgsArg(lowerer, expr.arguments[1], loc); - const optsNode = expr.arguments[2]; + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const argv = lowerChildArgsArg(lowerer, expr.arguments.nodes[1], loc); + const optsNode = expr.arguments.nodes[2]; let timeout: IrExpr = numLit(0, loc); let killSignal: IrExpr = { kind: "strLit", value: "", type: STRING, loc }; @@ -152,8 +152,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo modeOf(t.value, 2); return; } - if (ts.isArrayLiteralExpression(node) && node.elements.length === 3) { - node.elements.forEach((el, i) => { + if (ts.isArrayLiteralExpression(node) && node.elements.nodes.length === 3) { + node.elements.nodes.forEach((el, i) => { const et = lowerer.typeOf(el); if (!et.isStringLiteralType()) { lowerer.noLowering("spawnSync stdio tuple entries beyond string literals", el); @@ -182,7 +182,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo 'stdio takes a "pipe"/"ignore"/"inherit" literal (or a value typed as a union of those), or a 3-tuple of literals', ); }; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -280,21 +280,21 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo * `windowsHide` (a POSIX no-op). The bare `{ stdio: "ignore" }` shape * keeps its historical cp.spawn lowering. */ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( "spawn with this argument shape", expr, 'the supported form is spawn(command, args?, { stdio: "pipe" | "ignore" | "inherit", detached?, env?, cwd? })', ); } - const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const cmd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -338,7 +338,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S let plain = true; // exactly { stdio: "ignore" }: the historical libCall if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { // The conditional-spread idiom `...(isWindows ? {} : { detached: // true })` (either orientation): the one carried member supported // is `detached` with a boolean literal — the platform-conditional @@ -393,7 +393,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S // The 3-tuple form: every slot accepts a literal, including // "pipe" (child.stdin/stdout/stderr); stdout/stderr also accept // a number-typed fd (an openSync result — dup2'd into the child). - if (ts.isArrayLiteralExpression(m.value) && m.value.elements.length === 3) { + if (ts.isArrayLiteralExpression(m.value) && m.value.elements.nodes.length === 3) { const slot = (el: ts.Expression, which: 0 | 1 | 2): void => { const t = lowerer.typeOf(el); if (t.isStringLiteralType()) { @@ -434,7 +434,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S errFd = fd; } }; - m.value.elements.forEach((el, i) => slot(el, i as 0 | 1 | 2)); + m.value.elements.nodes.forEach((el, i) => slot(el, i as 0 | 1 | 2)); sawStdio = true; plain = false; break; @@ -512,7 +512,7 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S } if (shellEnabled && argsNode !== undefined) { const shellArgs = stripTypeCasts(argsNode); - if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.length !== 0) { + if (!ts.isArrayLiteralExpression(shellArgs) || shellArgs.elements.nodes.length !== 0) { lowerer.noLowering( "spawn with shell enabled and separate arguments", argsNode, @@ -551,9 +551,9 @@ export function lowerSpawnCall(lowerer: Lowerer, expr: ts.CallExpression, loc: S * Node's default JSON IPC channel is always present. */ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fork with this argument shape", @@ -561,24 +561,24 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr "use fork(staticModulePath[, stringArgs][, inlineOptions])", ); } - const targetPath = staticForkModulePath(lowerer.program, expr.arguments[0]!); + const targetPath = staticForkModulePath(lowerer.program, expr.arguments.nodes[0]!); const targetId = targetPath === null ? undefined : lowerer.forkTargetIdByPath.get(tsgoPath(resolve(targetPath))); if (targetId === undefined) { lowerer.noLowering( "fork with a runtime-valued module path", - expr.arguments[0]!, + expr.arguments.nodes[0]!, 'resolve a relative worker with new URL("./worker.ts", import.meta.url) or fileURLToPath(...) and bind it to a const if needed', ); } let argsNode: ts.Expression | undefined; let optsNode: ts.Expression | undefined; - if (expr.arguments.length === 3) { - argsNode = expr.arguments[1]; - optsNode = expr.arguments[2]; - } else if (expr.arguments.length === 2) { - const second = expr.arguments[1]!; + if (expr.arguments.nodes.length === 3) { + argsNode = expr.arguments.nodes[1]; + optsNode = expr.arguments.nodes[2]; + } else if (expr.arguments.nodes.length === 2) { + const second = expr.arguments.nodes[1]!; const secondType = lowerer.typeOf(second); const mapped = lowerer.mapTypeOf(secondType); if ( @@ -622,7 +622,7 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr }; if (optsNode && ts.isObjectLiteralExpression(optsNode)) { - for (const property of optsNode.properties) { + for (const property of optsNode.properties.nodes) { const member = optionMember(property); if (!member) { lowerer.noLowering( @@ -651,14 +651,14 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr break; case "stdio": { if (ts.isArrayLiteralExpression(member.value)) { - if (member.value.elements.length !== 4) { + if (member.value.elements.nodes.length !== 4) { lowerer.noLowering( "fork with this stdio tuple", member.value, 'use [stdin, stdout, stderr, "ipc"] with exactly four literal entries', ); } - const ipc = member.value.elements[3]!; + const ipc = member.value.elements.nodes[3]!; const ipcType = lowerer.typeOf(ipc); if (!ipcType.isStringLiteralType() || ipcType.value !== "ipc") { lowerer.noLowering( @@ -667,9 +667,9 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr 'the fourth stdio entry must be "ipc"', ); } - inMode = stdioMode(member.value.elements[0]!, "stdin"); - outMode = stdioMode(member.value.elements[1]!, "stdout"); - errMode = stdioMode(member.value.elements[2]!, "stderr"); + inMode = stdioMode(member.value.elements.nodes[0]!, "stdin"); + outMode = stdioMode(member.value.elements.nodes[1]!, "stdout"); + errMode = stdioMode(member.value.elements.nodes[2]!, "stderr"); } else { const mode = stdioMode(member.value, "scalar"); inMode = outMode = errMode = mode; @@ -743,20 +743,20 @@ export function lowerExecFileCall( ): IrExpr { const what = shell ? "exec" : "execFile"; if ( - expr.arguments.some(ts.isSpreadElement) || - expr.arguments.length < 2 || - expr.arguments.length > (shell ? 3 : 4) + expr.arguments.nodes.some(ts.isSpreadElement) || + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > (shell ? 3 : 4) ) { lowerer.noLowering( - `${what} with ${expr.arguments.length} arguments`, + `${what} with ${expr.arguments.nodes.length} arguments`, expr, shell ? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)" : "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)", ); } - const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const callbackNode = expr.arguments[expr.arguments.length - 1]!; + const first = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const callbackNode = expr.arguments.nodes[expr.arguments.nodes.length - 1]!; // exec: /bin/sh -c , execSync's lowering (the runtime reads the // command line back from argv[1] for failure messages); execFile: the // file and its args list. @@ -768,9 +768,15 @@ export function lowerExecFileCall( type: arrayOf(STRING), loc, } - : lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc); + : lowerChildArgsArg( + lowerer, + expr.arguments.nodes.length >= 3 ? expr.arguments.nodes[1] : undefined, + loc, + ); const optionsNode = - expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined; + expr.arguments.nodes.length === (shell ? 3 : 4) + ? expr.arguments.nodes[shell ? 1 : 2] + : undefined; if (optionsNode) { const options = optionsNode; if (!ts.isObjectLiteralExpression(options)) { @@ -792,7 +798,7 @@ export function lowerExecFileCall( ].includes(node.operatorToken.kind) && pureNumber(node.left) && pureNumber(node.right)); - for (const property of options.properties) { + for (const property of options.properties.nodes) { const member = optionMember(property); if (!member) lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index f24db09cf..416217716 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 @@ -92,7 +93,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol) if (builtinClassAliasOf(lowerer, symbol) === null) return false; const binding = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker.valueDeclarationOf(binding); + const decl = binding.valueDeclaration; if (decl === undefined || !ts.isVariableDeclaration(decl)) return false; if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false; if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false; @@ -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; @@ -134,7 +189,7 @@ export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolea * can name a class (an identifier or a property access), or null. */ function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null { if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl === undefined || !ts.isVariableDeclaration(decl) || diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 6de345dfe..e69011522 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -138,9 +138,7 @@ export function zlibStreamModeOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const mode = ZLIB_STREAM_MODES.get(target.name); if (mode === undefined) return null; - return lowerer.checker - .declarationsOf(target) - .some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) + return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) ? mode : null; } @@ -161,15 +159,13 @@ function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - d.parent.name?.text === "ZlibBase" && - isZlibFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + d.parent.name?.text === "ZlibBase" && + isZlibFallbackDecl(lowerer, d.parent), + ); } /** `stream.bytesWritten` on a zlib stream. */ @@ -197,7 +193,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 +300,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 +342,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 +369,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 +378,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 +419,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 +442,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 +462,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 +473,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 +490,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..611805d94 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -1,11 +1,11 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; -import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js"; +import { canonicalBuiltinModule, isJsSourceFile, locOf, requireSpecOf } from "../../program.js"; import { builtinConstLit } from "../surfaces.js"; import { HTTP2_CONSTANTS } from "../http2-constants.js"; import { CRYPTO_CONSTANTS } from "../crypto-tables.js"; import { ZLIB_CONSTANTS } from "../zlib-constants.js"; -import { F64, type IrExpr } from "../../../ir/ir.js"; +import { DYN, F64, type IrExpr, STRING, type SrcLoc } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; /** The builtin modules whose `constants` object bakes as literals at @@ -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 && @@ -106,7 +106,7 @@ export function builtinConstantsDestructureDecl( * its baked literal value. Null for every other binding. */ export function builtinConstantBindingOf(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isBindingElement(decl) || decl.dotDotDotToken || decl.initializer) return null; if (!ts.isObjectBindingPattern(decl.parent)) return null; const varDecl = decl.parent.parent; @@ -147,12 +147,30 @@ export function lowerFsConstantsProperty( COPYFILE_FICLONE_FORCE: 4, }; const value = own(MODES, expr.name.text); - if (value === undefined) { - lowerer.noLowering( - `fs.constants.${expr.name.text}`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + if (value === undefined) return fsConstantMemberRead(expr); return { kind: "numLit", value, type: F64, loc: locOf(expr) }; } + +/** fs.constants as a value: the target's null-prototype constants object, + * built by the runtime from its own libc's macros. */ +export function fsConstantsObject(loc: SrcLoc): IrExpr { + return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc }; +} + +/** `fs.constants.O_RDONLY` (any member the access-mode literals do not + * cover): a read of the runtime object, so a constant the target lacks + * reads as undefined in JavaScript and fails the number check in + * TypeScript, whose declarations promise every name. */ +export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr { + const loc = locOf(expr); + const read: IrExpr = { + kind: "dynKeyGet", + value: fsConstantsObject(loc), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }; + return isJsSourceFile(expr.getSourceFile()) + ? read + : { kind: "dynCheck", value: read, type: F64, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index bfac6128f..cd490bc87 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,10 +259,11 @@ 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); } + if (bi.member === "getRandomValues") return lowerCryptoGetRandomValues(lowerer, expr); if (bi.member === "randomBytes") { if (args.length === 1) return null; if (args.length !== 2) { @@ -465,8 +468,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 +823,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 }; @@ -876,7 +879,7 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean { return ( !!symbol && CIPHER_TYPE_NAMES.has(symbol.name) && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); } @@ -916,7 +919,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 +927,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 +949,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 +968,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 +1035,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 +1081,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 +1098,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 +1141,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); } @@ -1282,3 +1285,49 @@ export function lowerPromisifiedCryptoCall( "KeyObject", ); } + +/** `crypto.getRandomValues(view)` — the WebCrypto global, node:crypto's + * export and `webcrypto.getRandomValues`: a typed view fills in place and + * returns itself at its own type (float views, DataViews and views over + * 65536 bytes throw Node's DOMExceptions at run time); any other value + * takes the checked form. Null for other arities (the member fence keeps + * them). */ +export function lowerCryptoGetRandomValues( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) + return null; + const loc = locOf(call); + const value = lowerer.lowerExpr(call.arguments.nodes[0]!); + if (value.type.kind === "bytes") + return { kind: "libCall", fn: "crypto.getRandomValues", args: [value], type: value.type, loc }; + return { + kind: "libCall", + fn: "crypto.getRandomValuesDyn", + args: [lowerer.coerceInto(call.arguments.nodes[0]!, value, DYN)], + type: DYN, + loc, + }; +} + +/** Whether `receiver` names a WebCrypto Crypto object: the `crypto` + * global (also through globalThis) or node:crypto's `webcrypto`. */ +export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (ts.isIdentifier(receiver)) { + if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; + const imported = lowerer.builtinImportOf(receiver); + return imported?.module === "crypto" && imported.member === "webcrypto"; + } + if (ts.isPropertyAccessExpression(receiver) && !receiver.questionDotToken) { + if ( + receiver.name.text === "crypto" && + ts.isIdentifier(receiver.expression) && + lowerer.isStdlibGlobal(receiver.expression, "globalThis") + ) + return true; + const member = lowerer.builtinMemberOf(receiver); + return member?.module === "crypto" && member.member === "webcrypto"; + } + return false; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 0101a3ec3..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(), toUTCString(), 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); @@ -120,6 +126,10 @@ export function lowerDateCall( if (fn !== undefined) { return { kind: "libCall", fn, args: [receiver], type: F64, loc }; } + const textFn = DATE_TEXT_FNS.get(name); + if (textFn !== undefined) { + return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + } lowerer.noLowering( `Date.prototype.${name}`, call, @@ -137,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" @@ -148,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..0313cdd10 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -19,9 +19,9 @@ export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean { const sym = t.getSymbol(); return ( sym?.name === "ExecException" && - lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + sym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ); } @@ -209,8 +209,8 @@ export function lowerErrorIsErrorCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; if (lowerer.stdlibGlobalMember(access, "Error") !== "isError") return null; - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const caughtLocal = lowerer.caughtLocalOf(argNode); if (caughtLocal) { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts index d6864a2d9..05028e1df 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem-calls.ts @@ -38,12 +38,12 @@ export function lowerFsCallbackCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - if (expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("filesystem callback call with spread arguments", expr); const timestamp = ["utimes", "futimes", "lutimes"].includes(bi.member); const args: IrExpr = { kind: "dynArrLit", - elems: expr.arguments.map((arg, index) => + elems: expr.arguments.nodes.map((arg, index) => timestamp && (index === 1 || index === 2) ? lowerFsTimestampValue(lowerer, arg, loc) : lowerer.lowerExprExpecting(arg, DYN), @@ -71,11 +71,11 @@ export function lowerFsLinkOrStatfsCall( const symbolic = bi.member === "symlinkSync" || bi.member === "symlink"; const reading = bi.member === "readlinkSync" || bi.member === "readlink"; const arity = symbolic ? 3 : 2; - if (expr.arguments.length > arity || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > arity || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = Array.from({ length: arity }, (_, index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); if (reading) { @@ -117,11 +117,11 @@ export function lowerFsCpCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); const args = [0, 1, 2].map((index) => - expr.arguments[index] - ? lowerer.lowerExprExpecting(expr.arguments[index]!, DYN) + expr.arguments.nodes[index] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc }; @@ -134,18 +134,18 @@ export function lowerFsTimestampCall( loc: SrcLoc, name: string, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`${name} with this argument shape`, expr); - const path = expr.arguments[0] - ? lowerer.lowerExprExpecting(expr.arguments[0], DYN) + const path = expr.arguments.nodes[0] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[0], DYN) : dynUndefinedExpr(loc); return { kind: "libCall", fn: fn.fn, args: [ path, - lowerFsTimestampValue(lowerer, expr.arguments[1], loc), - lowerFsTimestampValue(lowerer, expr.arguments[2], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[1], loc), + lowerFsTimestampValue(lowerer, expr.arguments.nodes[2], loc), ], type: fn.result, loc, @@ -163,14 +163,14 @@ export function lowerFsVectorOrTruncateCall( const vector = bi.member !== "ftruncateSync"; const required = vector ? 2 : 1; if ( - expr.arguments.length < required || - expr.arguments.length > required + 1 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < required || + expr.arguments.nodes.length > required + 1 || + expr.arguments.nodes.some(ts.isSpreadElement) ) lowerer.noLowering(`${name} with this argument shape`, expr); - const args = [lowerer.lowerExprExpecting(expr.arguments[0]!, F64)]; - if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments[1]!, arrayOf(BYTES_U8))); - const optional = expr.arguments[required]; + const args = [lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64)]; + if (vector) args.push(lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, arrayOf(BYTES_U8))); + const optional = expr.arguments.nodes[required]; args.push( optional ? lowerBuiltinOptionalDefault(lowerer, optional, F64, numLit(vector ? -1 : 0, loc), vector) @@ -187,7 +187,7 @@ export function lowerFsNumericOpenCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length > 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length > 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("openSync with numeric flags and this argument shape", expr); } const bits: Record = { @@ -223,17 +223,17 @@ export function lowerFsNumericOpenCall( } return null; }; - const mask = flagsOf(expr.arguments[1]!); + const mask = flagsOf(expr.arguments.nodes[1]!); if (mask === null) { lowerer.noLowering( "openSync with computed numeric flags", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "use an inline bitwise OR of fs.constants.O_RDONLY/O_WRONLY/O_RDWR/O_CREAT/O_EXCL/O_NOFOLLOW/O_NONBLOCK/O_TRUNC/O_APPEND", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const mode = expr.arguments[2] - ? lowerer.lowerExprExpecting(expr.arguments[2]!, F64) + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const mode = expr.arguments.nodes[2] + ? lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64) : ({ kind: "numLit", value: 0o666, type: F64, loc } satisfies IrExpr); return { kind: "libCall", @@ -254,24 +254,24 @@ export function lowerFsPromisesOpenCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `fs.promises.open with ${expr.arguments.length} arguments`, + `fs.promises.open with ${expr.arguments.nodes.length} arguments`, expr, "use open(path[, stringFlags[, numericMode]])", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc }; const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc }; - const flags = expr.arguments[1] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[1]!, STRING, defaultFlags) + const flags = expr.arguments.nodes[1] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[1]!, STRING, defaultFlags) : defaultFlags; - const mode = expr.arguments[2] - ? lowerBuiltinOptionalDefault(lowerer, expr.arguments[2]!, F64, defaultMode) + const mode = expr.arguments.nodes[2] + ? lowerBuiltinOptionalDefault(lowerer, expr.arguments.nodes[2]!, F64, defaultMode) : defaultMode; return { kind: "libCall", @@ -292,16 +292,16 @@ export function lowerFsRenameCallbackCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - if (expr.arguments.length !== 3 || expr.arguments.some(ts.isSpreadElement)) { + if (expr.arguments.nodes.length !== 3 || expr.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `rename with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `rename with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, "the supported form is rename(oldPath, newPath, callback)", ); } - const oldPath = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const newPath = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); - let callback = lowerer.lowerExpr(expr.arguments[2]!); + const oldPath = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const newPath = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); + let callback = lowerer.lowerExpr(expr.arguments.nodes[2]!); // JS/checkJs callback values may arrive as checked-dynamic callables. // Adapt them to the one error-first slot; the runtime passes a dyn // Error on failure and null on success through the emitted thunk. @@ -310,7 +310,7 @@ export function lowerFsRenameCallbackCall( kind: "dynCheck", value: callback, type: funcOf([DYN], VOID), - loc: locOf(expr.arguments[2]!), + loc: locOf(expr.arguments.nodes[2]!), }; } let callbackOk = callback.type.kind === "func" && callback.type.params.length <= 1; @@ -337,7 +337,7 @@ export function lowerFsRenameCallbackCall( if (!callbackOk) { lowerer.unsupported( "SC1090", - expr.arguments[2]!, + expr.arguments.nodes[2]!, "fs.rename callbacks must accept at most one Error | null parameter", ); } @@ -345,7 +345,7 @@ export function lowerFsRenameCallbackCall( // void callback is expected; Node ignores that value. Normalize the // closure to the runtime's void callback ABI after validating its // error-first parameter shape. - callback = voidizedCallback(lowerer, callback, locOf(expr.arguments[2]!)); + callback = voidizedCallback(lowerer, callback, locOf(expr.arguments.nodes[2]!)); return { kind: "libCall", fn: "fs.renameCb", @@ -366,17 +366,17 @@ export function lowerFsReadSyncCall( const supported = "use readSync(fd, buffer), readSync(fd, buffer, { offset?, length?, position? }), or readSync(fd, buffer, offset, length[, position]); bigint positions and non-literal options objects have no lowering"; if ( - expr.arguments.length < 2 || - expr.arguments.length > 5 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 2 || + expr.arguments.nodes.length > 5 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `readSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `readSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -386,8 +386,8 @@ export function lowerFsReadSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const optionsNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const optionsNode = expr.arguments.nodes[2]!; let valueNode = optionsNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -403,12 +403,12 @@ export function lowerFsReadSyncCall( lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported); } const args: IrExpr[] = [ - lowerer.lowerExprExpecting(expr.arguments[0]!, F64), - lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), ]; - const positionNode = expr.arguments[4]; + const positionNode = expr.arguments.nodes[4]; let positionValueNode = positionNode; while (positionValueNode && ts.isParenthesizedExpression(positionValueNode)) { positionValueNode = positionValueNode.expression; @@ -441,14 +441,14 @@ export function lowerFsWriteSyncCall( ): IrExpr { const supported = 'use writeSync(fd, buffer[, offset[, length[, position]]]), writeSync(fd, buffer, { offset?, length?, position? }), or writeSync(fd, string[, position[, "utf8"]]); non-literal options objects, bigint positions, and other encodings have no lowering'; - if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2) { + if (expr.arguments.nodes.some(ts.isSpreadElement) || expr.arguments.nodes.length < 2) { lowerer.noLowering( - `writeSync with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`, + `writeSync with ${expr.arguments.nodes.length} argument${expr.arguments.nodes.length === 1 ? "" : "s"}`, expr, supported, ); } - const dataNode = expr.arguments[1]!; + const dataNode = expr.arguments.nodes[1]!; const dataType = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const position = (node: ts.Expression | undefined): IrExpr => { let valueNode = node; @@ -474,7 +474,7 @@ export function lowerFsWriteSyncCall( "byte writes take Uint8Array/Buffer data", ); } - if (expr.arguments.length === 2) { + if (expr.arguments.nodes.length === 2) { return lowerFsSyncBufferWindow( lowerer, expr, @@ -484,8 +484,8 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length === 3) { - const thirdNode = expr.arguments[2]!; + if (expr.arguments.nodes.length === 3) { + const thirdNode = expr.arguments.nodes[2]!; let valueNode = thirdNode; while (ts.isParenthesizedExpression(valueNode)) valueNode = valueNode.expression; if (ts.isObjectLiteralExpression(valueNode) || valueNode.kind === ts.SyntaxKind.NullKeyword) { @@ -514,37 +514,37 @@ export function lowerFsWriteSyncCall( supported, ); } - if (expr.arguments.length !== 4 && expr.arguments.length !== 5) { + if (expr.arguments.nodes.length !== 4 && expr.arguments.nodes.length !== 5) { lowerer.noLowering( - `writeSync of Buffer data with ${expr.arguments.length} arguments`, + `writeSync of Buffer data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeSync", args: [ fd, lowerer.lowerExprExpecting(dataNode, BYTES_U8), - lowerer.lowerExprExpecting(expr.arguments[2]!, F64), - lowerer.lowerExprExpecting(expr.arguments[3]!, F64), - position(expr.arguments[4]), + lowerer.lowerExprExpecting(expr.arguments.nodes[2]!, F64), + lowerer.lowerExprExpecting(expr.arguments.nodes[3]!, F64), + position(expr.arguments.nodes[4]), ], type: F64, loc, }; } if (dataType?.kind === "string") { - if (expr.arguments.length > 4) { + if (expr.arguments.nodes.length > 4) { lowerer.noLowering( - `writeSync of string data with ${expr.arguments.length} arguments`, + `writeSync of string data with ${expr.arguments.nodes.length} arguments`, expr, supported, ); } - const encNode = expr.arguments[3]; + const encNode = expr.arguments.nodes[3]; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode !== undefined) { const encType = lowerer.typeOf(encNode); @@ -559,11 +559,16 @@ export function lowerFsWriteSyncCall( // a call/getter whose type is the accepted literal cannot vanish. enc = lowerer.lowerExprExpecting(encNode, STRING); } - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); return { kind: "libCall", fn: "fs.writeStrSync", - args: [fd, lowerer.lowerExprExpecting(dataNode, STRING), position(expr.arguments[2]), enc], + args: [ + fd, + lowerer.lowerExprExpecting(dataNode, STRING), + position(expr.arguments.nodes[2]), + enc, + ], type: F64, loc, }; @@ -585,22 +590,22 @@ export function lowerFsReadDescriptorCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const fd = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - if (expr.arguments.length === 1) { + const fd = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFdSyncBytes", args: [fd], type: BYTES_U8, loc }; } - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if ( - expr.arguments.length !== 2 || + expr.arguments.nodes.length !== 2 || !(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8")) ) { lowerer.noLowering( `readFileSync(fd) with a non-"utf8" encoding`, - expr.arguments[1] ?? expr, + expr.arguments.nodes[1] ?? expr, 'only utf8 reads are supported: readFileSync(fd, "utf8")', ); } - const enc = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const enc = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); return { kind: "libCall", fn: "fs.readFdSync", args: [fd, enc], type: STRING, loc }; } @@ -620,8 +625,9 @@ export function lowerFsEncodedRead( descriptor: boolean, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; - const options = expr.arguments[1]!; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; + const options = expr.arguments.nodes[1]!; const optionsType = lowerer.mapTypeOf(lowerer.typeOf(options)); if (optionsType?.kind !== "string" && optionsType?.kind !== "record") return null; if (optionsType.kind === "record") { @@ -634,7 +640,7 @@ export function lowerFsEncodedRead( return null; } const sourceType = descriptor ? F64 : STRING; - const source = lowerer.lowerExprExpecting(expr.arguments[0]!, sourceType); + const source = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, sourceType); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -674,10 +680,11 @@ export function lowerFsPromisesEncodedRead( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr | null { - if (expr.arguments.length !== 2 || expr.arguments.some(ts.isSpreadElement)) return null; + if (expr.arguments.nodes.length !== 2 || expr.arguments.nodes.some(ts.isSpreadElement)) + return null; const result = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (result?.kind !== "promise" || result.inner.kind !== "string") return null; - const options = expr.arguments[1]!; + const options = expr.arguments.nodes[1]!; const optionsTsType = lowerer.typeOf(options); if ( optionsTsType.isStringLiteralType() && @@ -695,7 +702,7 @@ export function lowerFsPromisesEncodedRead( ) return null; } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const encoding: IrExpr = optionsType.kind === "string" ? lowerer.lowerExprExpecting(options, STRING) @@ -729,15 +736,15 @@ export function lowerFsOpendirCall( loc: SrcLoc, ): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 2 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 2 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { - lowerer.noLowering(`${member} with ${expr.arguments.length} arguments`, expr); + lowerer.noLowering(`${member} with ${expr.arguments.nodes.length} arguments`, expr); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); let bufferSize: IrExpr = { kind: "numLit", value: 32, type: F64, loc }; - const optsNode = expr.arguments[1]; + const optsNode = expr.arguments.nodes[1]; if (optsNode && !(ts.isIdentifier(optsNode) && optsNode.text === "undefined")) { const fence = (): never => lowerer.noLowering( @@ -746,7 +753,7 @@ export function lowerFsOpendirCall( "pass an object literal with a numeric bufferSize and a utf8 encoding; recursive walks and other encodings have no lowering", ); if (!ts.isObjectLiteralExpression(optsNode)) fence(); - for (const property of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const property of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const option = optionMember(property); if (!option) fence(); else if (option.name === "bufferSize") @@ -775,7 +782,7 @@ export function lowerFsStreamCreate( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments, + expr.arguments.nodes, member === "createReadStream", `fs.${member}`, expr, @@ -795,7 +802,7 @@ export function lowerFsMkdirSyncCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -805,7 +812,7 @@ export function lowerFsMkdirSyncCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; if (!recursive) { return mode @@ -833,7 +840,7 @@ export function lowerFsPromisesMkdirCall( expr: ts.CallExpression, loc: SrcLoc, ): IrExpr { - const optsNode = expr.arguments[1]!; + const optsNode = expr.arguments.nodes[1]!; const options = parseMkdirOptions(optsNode); if (!options) { lowerer.noLowering( @@ -843,7 +850,7 @@ export function lowerFsPromisesMkdirCall( ); } const { recursive, modeNode } = options; - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null; const type: IrType = { kind: "promise", inner: VOID }; if (!recursive) { @@ -866,18 +873,18 @@ export function lowerFsPromisesMkdirCall( // historical rmOptsSync lowering unchanged. export function lowerFsRemoveCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { const opts = literalBoolOptions( - expr.arguments[1]!, + expr.arguments.nodes[1]!, ["recursive", "force"], ["maxRetries", "retryDelay"], ); if (opts === null) { lowerer.noLowering( "rmSync with an options argument beyond { recursive, force, maxRetries, retryDelay }", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "the recursive/force flags must be boolean literals; maxRetries/retryDelay are numbers; other options have no lowering", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const recursive = boolLit(opts.bools["recursive"] === true, loc); const force = boolLit(opts.bools["force"] === true, loc); @@ -916,7 +923,7 @@ export function lowerFsWriteBytesCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr | null { - const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!)); + const dataIr = lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!)); if (dataIr?.kind === "union") { const arms = lowerer.unions.get(dataIr.unionId)?.arms; if ( @@ -930,12 +937,12 @@ export function lowerFsWriteBytesCall( if (dataIr.elem !== "u8") { lowerer.noLowering( `${bi.member} of '${lowerer.fmt(dataIr)}' data`, - expr.arguments[1]!, + expr.arguments.nodes[1]!, "byte writes take Uint8Array/Buffer data", ); } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); if (bi.module === "fs/promises") { return { kind: "libCall", @@ -963,7 +970,7 @@ export function lowerFsWriteOptionsCall( loc: SrcLoc, promiseWriteOptions: boolean, ): IrExpr { - const optsNode = expr.arguments[2]!; + const optsNode = expr.arguments.nodes[2]!; const operation = promiseWriteOptions ? "fs.promises.writeFile" : "writeFileSync"; const plainFn: IrLibFn = promiseWriteOptions ? "fsp.writeFile" : "fs.writeFileSync"; const modeFn: IrLibFn = promiseWriteOptions ? "fsp.writeFileMode" : "fs.writeFileModeSync"; @@ -974,8 +981,8 @@ export function lowerFsWriteOptionsCall( // values in object-literal order before issuing the write; otherwise a // call/getter statically typed as the accepted utf8 literal can vanish. const finishWrite = (optionValues: WriteOptionValue[]): IrExpr => { - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); - const data = lowerer.lowerExprExpecting(expr.arguments[1]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); + const data = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, STRING); const pathLocal = lowerer.declareHiddenLocal("%writePath", STRING); const dataLocal = lowerer.declareHiddenLocal("%writeData", STRING); const stmts: IrStmt[] = [ @@ -1021,7 +1028,7 @@ export function lowerFsWriteOptionsCall( if ( t.isStringLiteralType() && (t.value === "utf8" || t.value === "utf-8") && - lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind === "string" + lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string" ) { return finishWrite([{ kind: "effect", value: lowerer.lowerExprExpecting(optsNode, STRING) }]); } @@ -1029,7 +1036,7 @@ export function lowerFsWriteOptionsCall( const optionValues: WriteOptionValue[] = []; let ok = ts.isObjectLiteralExpression(optsNode); if (ok) { - for (const p of (optsNode as ts.ObjectLiteralExpression).properties) { + for (const p of (optsNode as ts.ObjectLiteralExpression).properties.nodes) { const m = optionMember(p); if (!m) { ok = false; @@ -1066,7 +1073,7 @@ export function lowerFsWriteOptionsCall( } } } - if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments[1]!))?.kind !== "string") { + if (!ok || lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind !== "string") { lowerer.noLowering( `${operation} with 3 arguments`, optsNode, @@ -1089,20 +1096,20 @@ export function lowerFsReadPathCall( // non-string paths throw ERR_INVALID_ARG_TYPE where the dynCheck // throws its path-annotated TypeError. The fd forms (a number // argument) and literal-utf8 reads keep their existing lowerings. - const pathV = lowerer.lowerExpr(expr.arguments[0]!); + const pathV = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (pathV.type.kind === "string" || pathV.type.kind === "dyn") { const pathArg: IrExpr = pathV.type.kind === "dyn" ? { kind: "dynCheck", value: pathV, type: STRING, loc } : pathV; - if (expr.arguments.length === 1) { + if (expr.arguments.nodes.length === 1) { return { kind: "libCall", fn: "fs.readFileSyncBuf", args: [pathArg], type: BYTES_U8, loc }; } // Runtime nullish and empty encodings read Buffers, supported names // read strings, and invalid names throw ERR_INVALID_ARG_VALUE. A // non-dyn encoding falls through to the literal-utf8 lowering // below (the discarded probe IR never emits). - const encT = lowerer.typeOf(expr.arguments[1]!); + const encT = lowerer.typeOf(expr.arguments.nodes[1]!); if (!(encT.isStringLiteralType() && (encT.value === "utf8" || encT.value === "utf-8"))) { - const enc = lowerer.lowerExpr(expr.arguments[1]!); + const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!); if (enc.type.kind === "dyn") { return { kind: "libCall", @@ -1125,7 +1132,7 @@ function parseMkdirOptions( if (!ts.isObjectLiteralExpression(node)) return null; let recursive = false; let modeNode: ts.Expression | null = null; - for (const property of node.properties) { + for (const property of node.properties.nodes) { const member = optionMember(property); if (!member) return null; if (member.name === "recursive") { diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index a091094de..3fbea04aa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -48,8 +48,8 @@ export function lowerFsSyncBufferWindow( source: FsSyncBufferWindowSource, supported: string, ): IrExpr { - const fdValue = lowerer.lowerExprExpecting(expr.arguments[0]!, F64); - const bufferValue = lowerer.lowerExprExpecting(expr.arguments[1]!, BYTES_U8); + const fdValue = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64); + const bufferValue = lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8); const fdLocal = lowerer.declareHiddenLocal("%fsSyncFd", F64); const bufferLocal = lowerer.declareHiddenLocal("%fsSyncBuffer", BYTES_U8); const stmts: IrStmt[] = [ @@ -88,7 +88,7 @@ export function lowerFsSyncBufferWindow( if (!ts.isObjectLiteralExpression(source.node)) { lowerer.noLowering(`${fn} with a non-literal options argument`, source.node, supported); } - for (const property of (source.node as ts.ObjectLiteralExpression).properties) { + for (const property of (source.node as ts.ObjectLiteralExpression).properties.nodes) { const member = optionMember(property); if (member === null) { lowerer.noLowering(`${fn} with a non-plain options member`, property, supported); @@ -189,11 +189,12 @@ export function lowerFsToUnixTimestampCall( loc: SrcLoc, ): IrExpr | null { if (bi.module !== "fs" || bi.member !== "_toUnixTimestamp") return null; - if (expr.arguments.length !== 1 || ts.isSpreadElement(expr.arguments[0]!)) return null; + if (expr.arguments.nodes.length !== 1 || ts.isSpreadElement(expr.arguments.nodes[0]!)) + return null; return { kind: "libCall", fn: "fs.toUnixTimestamp", - args: [lowerFsTimestampValue(lowerer, expr.arguments[0], loc)], + args: [lowerFsTimestampValue(lowerer, expr.arguments.nodes[0], loc)], type: F64, loc, }; @@ -208,7 +209,7 @@ function opendirLowers(lowerer: Lowerer, args: readonly ts.Expression[]): boolea const options = args[1]; if (!options || (ts.isIdentifier(options) && options.text === "undefined")) return true; if (!ts.isObjectLiteralExpression(options)) return false; - return options.properties.every((property) => { + return options.properties.nodes.every((property) => { const option = optionMember(property); if (!option) return false; if (option.name === "bufferSize") @@ -250,7 +251,7 @@ export function lowerFsLadderCall( } if (bi.module !== "fs" && bi.module !== "fs/promises") return null; if (!isJsSourceFile(expr.getSourceFile())) return null; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.some(ts.isSpreadElement)) return null; // Every ladder argument crosses as a dyn value; an argument that // cannot leaves the historical fence in place. @@ -304,12 +305,12 @@ export function lowerFsLadderCall( ts.isIdentifier(pathNode.expression) && pathNode.expression.text === "URL" && lowerer.mapTypeOf(lowerer.typeOf(pathNode))?.kind === "url" && - pathNode.arguments?.length === 1 && - ts.isStringLiteral(pathNode.arguments[0]!) + pathNode.arguments?.nodes.length === 1 && + ts.isStringLiteral(pathNode.arguments.nodes[0]!) ) { let parsed: URL | null = null; try { - parsed = new URL(pathNode.arguments[0]!.text); + parsed = new URL(pathNode.arguments.nodes[0]!.text); } catch { parsed = null; } @@ -512,8 +513,8 @@ export function lowerStringOrBytesWrite( kind: "call", callee: helper, args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, STRING), - lowerer.lowerExprExpecting(call.arguments[1]!, dataType), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, dataType), ], type: VOID, loc, @@ -648,7 +649,8 @@ export function lowerFileHandleMethodCall( }; if (name === "sync" || name === "datasync") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "sync" ? "fileHandle.sync" : "fileHandle.datasync", @@ -658,34 +660,34 @@ export function lowerFileHandleMethodCall( }; } if (name === "truncate" || name === "chmod") { - if (call.arguments.length > 1 || (name === "chmod" && call.arguments.length !== 1)) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || (name === "chmod" && call.arguments.nodes.length !== 1)) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); return { kind: "libCall", fn: name === "truncate" ? "fileHandle.truncate" : "fileHandle.chmod", - args: [receiver(), num(call.arguments[0], 0).value], + args: [receiver(), num(call.arguments.nodes[0], 0).value], type: promise(VOID), loc, }; } if (name === "utimes") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("FileHandle.utimes with this argument shape", call); return { kind: "libCall", fn: "fileHandle.utimes", args: [ receiver(), - lowerFsTimestampValue(lowerer, call.arguments[0], loc), - lowerFsTimestampValue(lowerer, call.arguments[1], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[0], loc), + lowerFsTimestampValue(lowerer, call.arguments.nodes[1], loc), ], type: promise(VOID), loc, }; } if (name === "readv" || name === "writev") { - if (call.arguments.length < 1 || call.arguments.length > 2) - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") lowerer.badType(call, lowerer.typeOf(call)); @@ -694,15 +696,16 @@ export function lowerFileHandleMethodCall( fn: name === "readv" ? "fileHandle.readv" : "fileHandle.writev", args: [ receiver(), - lowerer.lowerExprExpecting(call.arguments[0]!, arrayOf(BYTES_U8)), - num(call.arguments[1], -1).value, + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, arrayOf(BYTES_U8)), + num(call.arguments.nodes[1], -1).value, ], type, loc, }; } if (name === "close" || name === "stat") { - if (call.arguments.length !== 0) lowerer.noLowering(`FileHandle.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) + lowerer.noLowering(`FileHandle.${name} with arguments`, call); return { kind: "libCall", fn: name === "close" ? "fileHandle.close" : "fileHandle.stat", @@ -713,12 +716,12 @@ export function lowerFileHandleMethodCall( } if (name === "readFile") { - if (call.arguments.length > 1) { - lowerer.noLowering(`FileHandle.readFile with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`FileHandle.readFile with ${call.arguments.nodes.length} arguments`, call); } const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise") lowerer.badType(call, lowerer.typeOf(call)); - const encoding = utf8(call.arguments[0]); + const encoding = utf8(call.arguments.nodes[0]); if (typeEquals(type.inner, BYTES_U8)) { return { kind: "libCall", @@ -739,14 +742,14 @@ export function lowerFileHandleMethodCall( } if (name === "writeFile" || name === "appendFile") { - if (call.arguments.length < 1 || call.arguments.length > 2) { - lowerer.noLowering(`FileHandle.${name} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2) { + lowerer.noLowering(`FileHandle.${name} with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); // Evaluate a supplied utf8 encoding even for Buffer data, matching // Node's argument order; it does not affect the bytes. - const encoding = utf8(call.arguments[1]); + const encoding = utf8(call.arguments.nodes[1]); if (dataT?.kind === "string") { return { kind: "libCall", @@ -773,10 +776,10 @@ export function lowerFileHandleMethodCall( } if (name === "read" || name === "write") { - const firstNode = call.arguments[0]; + const firstNode = call.arguments.nodes[0]; const firstT = firstNode ? lowerer.mapTypeOf(lowerer.typeOf(firstNode)) : undefined; - const secondT = call.arguments[1] - ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments[1]!)) + const secondT = call.arguments.nodes[1] + ? lowerer.mapTypeOf(lowerer.typeOf(call.arguments.nodes[1]!)) : undefined; const readOptions = name === "read" && @@ -784,7 +787,7 @@ export function lowerFileHandleMethodCall( const bufferOptions = firstT?.kind === "bytes" && firstT.elem === "u8" && secondT?.kind === "record"; if (readOptions || bufferOptions) { - if (call.arguments.length > (bufferOptions ? 2 : 1)) + if (call.arguments.nodes.length > (bufferOptions ? 2 : 1)) lowerer.noLowering(`FileHandle.${name} options with extra arguments`, call); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") @@ -799,7 +802,7 @@ export function lowerFileHandleMethodCall( let buffer = bufferOptions ? stage(lowerer.lowerExprExpecting(firstNode!, BYTES_U8), "%fhWindowBuffer") : undefined; - const optionsNode = bufferOptions ? call.arguments[1] : firstNode; + const optionsNode = bufferOptions ? call.arguments.nodes[1] : firstNode; const options = optionsNode ? stage(lowerer.lowerExpr(optionsNode), "%fhWindowOptions") : undefined; @@ -858,21 +861,21 @@ export function lowerFileHandleMethodCall( } if (name === "read") { - if (call.arguments.length < 1 || call.arguments.length > 4) { + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { lowerer.noLowering( - `FileHandle.read with ${call.arguments.length} arguments`, + `FileHandle.read with ${call.arguments.nodes.length} arguments`, call, "use read(buffer[, offset[, length[, position]]])", ); } - const buffer = lowerer.lowerExprExpecting(call.arguments[0]!, BYTES_U8); + const buffer = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BYTES_U8); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.read", @@ -883,19 +886,19 @@ export function lowerFileHandleMethodCall( } if (name === "write") { - if (call.arguments.length < 1 || call.arguments.length > 4) { - lowerer.noLowering(`FileHandle.write with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 4) { + lowerer.noLowering(`FileHandle.write with ${call.arguments.nodes.length} arguments`, call); } - const dataNode = call.arguments[0]!; + const dataNode = call.arguments.nodes[0]!; const dataT = lowerer.mapTypeOf(lowerer.typeOf(dataNode)); const type = lowerer.mapTypeOf(lowerer.typeOf(call)); if (type?.kind !== "promise" || type.inner.kind !== "record") { lowerer.badType(call, lowerer.typeOf(call)); } if (dataT?.kind === "bytes" && dataT.elem === "u8") { - const offset = num(call.arguments[1], 0); - const length = num(call.arguments[2], -1); - const position = num(call.arguments[3], -1); + const offset = num(call.arguments.nodes[1], 0); + const length = num(call.arguments.nodes[2], -1); + const position = num(call.arguments.nodes[3], -1); return { kind: "libCall", fn: "fileHandle.writeBytes", @@ -912,14 +915,14 @@ export function lowerFileHandleMethodCall( }; } if (dataT?.kind === "string") { - if (call.arguments.length > 3) { + if (call.arguments.nodes.length > 3) { lowerer.noLowering( - `FileHandle.write(string) with ${call.arguments.length} arguments`, + `FileHandle.write(string) with ${call.arguments.nodes.length} arguments`, call, 'use write(string[, position[, "utf8"]])', ); } - const position = num(call.arguments[1], -1); + const position = num(call.arguments.nodes[1], -1); return { kind: "libCall", fn: "fileHandle.writeStr", @@ -927,7 +930,7 @@ export function lowerFileHandleMethodCall( receiver(), lowerer.lowerExprExpecting(dataNode, STRING), position.value, - utf8(call.arguments[2]), + utf8(call.arguments.nodes[2]), ], type, loc, @@ -960,7 +963,7 @@ function lowerFileHandleStreamCall( receiver: IrExpr, loc: SrcLoc, ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.some(ts.isSpreadElement) || args.length > 1) { lowerer.noLowering(`FileHandle.${name} with ${args.length} arguments`, call); } @@ -1021,7 +1024,7 @@ export function lowerDirMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (DIR_METHODS.has(name) && call.arguments.length === 0) { + if (DIR_METHODS.has(name) && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExprExpecting(access.expression, DIR_T); if (name === "closeSync") return { kind: "libCall", fn: "dir.closeSync", args: [receiver], type: VOID, loc }; @@ -1093,7 +1096,7 @@ export function lowerStatsMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const fn = Object.hasOwn(STATS_TYPE_TESTS, name) ? STATS_TYPE_TESTS[name] : undefined; - if (fn && call.arguments.length === 0) { + if (fn && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn, args: [receiver], type: BOOL, loc: locOf(call) }; } @@ -1120,9 +1123,9 @@ export function lowerStatsMethodCall( * alive until watcher.close(). */ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { if ( - expr.arguments.length < 1 || - expr.arguments.length > 3 || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.length < 1 || + expr.arguments.nodes.length > 3 || + expr.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( "fs.watch with this argument shape", @@ -1130,16 +1133,17 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: "the supported forms are watch(path[, options][, listener])", ); } - const hasOptions = expr.arguments.length >= 2 && ts.isObjectLiteralExpression(expr.arguments[1]!); - if (expr.arguments.length === 3 && !hasOptions) { + const hasOptions = + expr.arguments.nodes.length >= 2 && ts.isObjectLiteralExpression(expr.arguments.nodes[1]!); + if (expr.arguments.nodes.length === 3 && !hasOptions) { lowerer.noLowering( "fs.watch with a non-literal options argument", - expr.arguments[1]!, + expr.arguments.nodes[1]!, "pass the options as an object literal: watch(path, { recursive?, persistent?, encoding? }, listener)", ); } if (hasOptions) { - for (const prop of (expr.arguments[1] as ts.ObjectLiteralExpression).properties) { + for (const prop of (expr.arguments.nodes[1] as ts.ObjectLiteralExpression).properties.nodes) { if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) { lowerer.noLowering( "fs.watch options with computed keys or spreads", @@ -1202,14 +1206,14 @@ export function lowerFsWatchCall(lowerer: Lowerer, expr: ts.CallExpression, loc: // An undocumented key, dropped like Node drops it. } } - const path = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING); const args: IrExpr[] = [path]; const listenerArg = hasOptions - ? expr.arguments.length === 3 - ? expr.arguments[2]! + ? expr.arguments.nodes.length === 3 + ? expr.arguments.nodes[2]! : null - : expr.arguments.length === 2 - ? expr.arguments[1]! + : expr.arguments.nodes.length === 2 + ? expr.arguments.nodes[1]! : null; if (listenerArg !== null) { const cb = lowerer.lowerExpr(listenerArg); @@ -1253,7 +1257,7 @@ export function lowerWatcherMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - if (name === "close" && call.arguments.length === 0) { + if (name === "close" && call.arguments.nodes.length === 0) { const receiver = lowerer.lowerExpr(access.expression); return { kind: "libCall", fn: "watcher.close", args: [receiver], type: VOID, loc }; } @@ -1291,7 +1295,7 @@ export function lowerFsReaddirTypesCall( ): IrExpr { const operation = promiseForm ? "fs.promises.readdir" : "readdirSync"; const spelling = promiseForm ? "readdir" : "readdirSync"; - const optsNode = call.arguments[1]!; + const optsNode = call.arguments.nodes[1]!; if (!ts.isObjectLiteralExpression(optsNode)) { lowerer.noLowering( `${operation} with a non-literal options argument`, @@ -1300,7 +1304,7 @@ export function lowerFsReaddirTypesCall( ); } let sawWithFileTypes = false; - for (const p of optsNode.properties) { + for (const p of optsNode.properties.nodes) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1372,7 +1376,7 @@ export function lowerFsReaddirTypesCall( ) { fence(); } - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); return { kind: "libCall", fn: promiseForm ? "fsp.readdirTypes" : "fs.readdirTypesSync", @@ -1400,8 +1404,8 @@ export function lowerFsStatOptionsCall( ): IrExpr { const spelling = noFollow ? "lstatSync" : "statSync"; const plainFn: IrLibFn = noFollow ? "fs.lstatSync" : "fs.statSync"; - const optsNode = call.arguments[1]!; - const path = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); + const optsNode = call.arguments.nodes[1]!; + const path = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); // The path binds first, then the options' effects, then the stat runs. const plainAfter = (effect: IrExpr | null): IrExpr => { if (effect === null) return { kind: "libCall", fn: plainFn, args: [path], type: STATS_T, loc }; @@ -1481,7 +1485,7 @@ export function lowerFsStatOptionsCall( lowerer.noLowering(`${spelling} with this options value`, optsNode, "pass an object literal"); } } - for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties : []) { + for (const p of ts.isObjectLiteralExpression(optsNode) ? optsNode.properties.nodes : []) { const m = optionMember(p); if (!m) { lowerer.noLowering( @@ -1578,7 +1582,7 @@ export function lowerDirentMethodCall( lowerer.checker.getSymbolAtLocation(access.name), ); } - if (call.arguments.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); + if (call.arguments.nodes.length !== 0) lowerer.noLowering(`Dirent.${name} with arguments`, call); const dtype: IrExpr = { kind: "recordGet", obj: receiver, diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts index 389da8dc0..f988c204d 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-glob.ts @@ -40,7 +40,7 @@ function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean /** An exclude function receives Node's Dirent objects when withFileTypes is * set, which have no native representation here. */ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: string): void { - const options = call.arguments[1]; + const options = call.arguments.nodes[1]; if (!options) return; const exclude = lowerer.checker.getPropertyOfType(lowerer.typeOf(options), "exclude"); if (!exclude) return; @@ -54,10 +54,10 @@ function fenceDirentExclude(lowerer: Lowerer, call: ts.CallExpression, what: str } function globArgs(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`${what} with spread arguments`, call, "write the arguments positionally"); } - return call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + return call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); } /** `globSync(pattern, options?)`: string[] or Dirent[]. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 77ad8fa2e..22e524690 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -34,15 +34,13 @@ function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { export function isFsStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(access.name); if (!symbol) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (d) => - d.parent !== undefined && - ts.isClassDeclaration(d.parent) && - (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && - isFsFallbackDecl(lowerer, d.parent), - ); + return symbol.declarations.some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + (d.parent.name?.text === "ReadStream" || d.parent.name?.text === "WriteStream") && + isFsFallbackDecl(lowerer, d.parent), + ); } /** bytesRead / bytesWritten / pending / path reads. Null for other @@ -87,7 +85,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); @@ -156,9 +154,9 @@ export function fsStreamClassOf( symbol: ts.Symbol | null | undefined, ): "ReadStream" | "WriteStream" | null { if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null; - const declared = lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d)); + const declared = symbol.declarations.some( + (d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d), + ); return declared ? symbol.name : null; } @@ -171,7 +169,7 @@ export function lowerFsStreamNew( ): IrExpr { return fsStreamCreateExpr( lowerer, - expr.arguments ?? [], + expr.arguments?.nodes ?? [], cls === "ReadStream", `new fs.${cls}`, expr, diff --git a/packages/compiler/src/frontend/lowering/builtins/ipc.ts b/packages/compiler/src/frontend/lowering/builtins/ipc.ts index 630927a60..cf9d96117 100644 --- a/packages/compiler/src/frontend/lowering/builtins/ipc.ts +++ b/packages/compiler/src/frontend/lowering/builtins/ipc.ts @@ -89,20 +89,20 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr { export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr { const loc = locOf(call); if ( - call.arguments.length < 1 || - call.arguments.length > 2 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 2 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( - `process.send with ${call.arguments.length} arguments`, + `process.send with ${call.arguments.nodes.length} arguments`, call, "send(message[, callback]) is supported; handle transfer and options are not", ); } - const json = ipcJsonMessage(lowerer, call.arguments[0]!, loc); - if (call.arguments[1] === undefined) { + const json = ipcJsonMessage(lowerer, call.arguments.nodes[0]!, loc); + if (call.arguments.nodes[1] === undefined) { return { kind: "libCall", fn: "process.send", args: [json], type: BOOL, loc }; } - const callback = ipcSendCallback(lowerer, call.arguments[1]); + const callback = ipcSendCallback(lowerer, call.arguments.nodes[1]); return { kind: "libCall", fn: "process.sendCb", args: [json, callback], type: BOOL, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index a08b108ad..b48030ba5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -36,9 +36,9 @@ export function lowerJsonMethodCall( if (member === null) return null; const loc = locOf(call); if ( - (member === "parse" && (call.arguments.length < 1 || call.arguments.length > 2)) || - (member === "stringify" && call.arguments.length > 3) || - call.arguments.some(ts.isSpreadElement) + (member === "parse" && (call.arguments.nodes.length < 1 || call.arguments.nodes.length > 2)) || + (member === "stringify" && call.arguments.nodes.length > 3) || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `JSON.${member} with extra or spread arguments`, @@ -47,8 +47,8 @@ export function lowerJsonMethodCall( ); } if (member === "parse") { - const text = lowerer.lowerExprExpecting(call.arguments[0]!, STRING); - const reviver = call.arguments[1]; + const text = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, STRING); + const reviver = call.arguments.nodes[1]; if (!reviver || jsonNullishArgument(lowerer, reviver)) { return { kind: "libCall", fn: "json.parse", args: [text], type: DYN, loc }; } @@ -56,8 +56,8 @@ export function lowerJsonMethodCall( return { kind: "libCall", fn: "json.parseReviver", args: [text, callback], type: DYN, loc }; } if (member === "stringify") { - const replacerNode = call.arguments[1]; - const spaceNode = call.arguments[2]; + const replacerNode = call.arguments.nodes[1]; + const spaceNode = call.arguments.nodes[2]; const runtimeOptions = (replacerNode && !jsonNullishArgument(lowerer, replacerNode) && @@ -68,19 +68,19 @@ export function lowerJsonMethodCall( !ts.isStringLiteral(spaceNode) && !ts.isNoSubstitutionTemplateLiteral(spaceNode) && !(ts.isPrefixUnaryExpression(spaceNode) && ts.isNumericLiteral(spaceNode.operand))); - if (runtimeOptions && call.arguments[0]) { + if (runtimeOptions && call.arguments.nodes[0]) { const args = [0, 1, 2].map((index) => - call.arguments[index] - ? lowerer.lowerExprExpecting(call.arguments[index]!, DYN) + call.arguments.nodes[index] + ? lowerer.lowerExprExpecting(call.arguments.nodes[index]!, DYN) : dynUndefinedExpr(loc), ); return { kind: "libCall", fn: "json.stringifyValue", args, type: DYN, loc }; } const indent = stringifySpaceIndent(lowerer, call); - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return lowerer.wrappedUndefined(lowerer.withUndefinedArm(STRING), loc)!; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let value = lowerer.lowerExpr(argNode); // Optional Map/array reads and partially narrowed unions keep their // storage representation when lowered as ordinary arguments. Select @@ -103,7 +103,7 @@ export function lowerJsonMethodCall( if (helper) value = { kind: "call", callee: helper, args: [value], type: narrowed, loc }; } } - const replacer = call.arguments[1]; + const replacer = call.arguments.nodes[1]; if (replacer && !jsonNullishArgument(lowerer, replacer)) { const callback = lowerJsonCallback(lowerer, replacer, "replacer"); if (value.type.kind === "undefinedT" || value.type.kind === "nullT") @@ -504,9 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = lowerer.checker.getSymbolAtLocation(node); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ); } @@ -573,12 +571,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..86d93de2c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -28,7 +28,7 @@ export function builtinImportOf( ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident ? lowerer.checker.getShorthandAssignmentValueSymbol(ident.parent) : lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin of the named import: a destructured require // binding (`const { readFileSync } = require("fs")`, renames via @@ -123,7 +123,7 @@ export function builtinImportOf( // keys the same tables under the builtin's own member name. if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = lowerer.checker.getAliasedSymbol(symbol); - const tdecl = lowerer.checker.declarationsOf(target)[0]; + const tdecl = target.declarations[0]; if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = tdecl.parent; @@ -153,7 +153,7 @@ export function esmImportBindingKind( ident: ts.Identifier, ): "namespace" | "named" | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined) return null; if (ts.isNamespaceImport(decl)) return "namespace"; return ts.isImportSpecifier(decl) ? "named" : null; @@ -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) && @@ -290,7 +290,7 @@ export function createRequireCalleeFileOf( } if (!ts.isIdentifier(e)) return null; const symbol = lowerer.checker.getSymbolAtLocation(e); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; return createRequireBindingDecl(lowerer, decl.name, decl.initializer) ? decl.getSourceFile() @@ -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 && @@ -391,7 +391,7 @@ export function createRequireProgramModuleDecl( * binding poison with the same module-naming diagnostic. */ export function fencedBuiltinImportOf(lowerer: Lowerer, ident: ts.Identifier): string | null { const symbol = lowerer.checker.getSymbolAtLocation(ident); - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twins: `const x = require("net")` and // `const { createServer } = require("net")` — the require statement 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/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts new file mode 100644 index 000000000..637edaf3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -0,0 +1,60 @@ +/* Node 24's `navigator` global (lib/internal/navigator.js), the part + * libraries read to tell runtimes apart: `typeof navigator` is "object" + * and `navigator.userAgent` is `Node.js/`. jose reads exactly + * that at module load (jwks/remote.js). Both fold to string literals, so + * nothing links. The other members (hardwareConcurrency, language, + * languages, platform, locks) and the object as a value keep a named + * refusal. */ +import * as ts from "../../ts7/adapter.js"; +import { strLit } from "../../../ir/build.js"; +import type { IrExpr } from "../../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import type { Lowerer } from "../lowerer.js"; + +/** True when `node` names Node's global `navigator`: the @types/node + * declaration, or, in a JavaScript file, the free name no declaration + * binds (Node defines it on the global object). */ +export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false; + if (lowerer.isStdlibGlobal(expr, "navigator")) return true; + return ( + isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined + ); +} + +/** `navigator.userAgent` (`Node.js/` of `nodeVersion`, the pinned + * release builtins/process.ts names), or the named refusal for another + * member. Null when the receiver is not the global. */ +export function lowerNavigatorMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + nodeVersion: string, +): IrExpr | null { + if (!isNavigatorGlobal(lowerer, access.expression)) return null; + if (access.name.text === "userAgent") { + return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access)); + } + return refuseNavigator( + lowerer, + access, + `navigator.${access.name.text}`, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** The refusal for the `navigator` object itself or an unlowered member. */ +export function refuseNavigator( + lowerer: Lowerer, + node: ts.Node, + what: string, + symbol?: ts.Symbol, +): never { + return lowerer.noLowering( + what, + node, + "of Node's navigator global only typeof navigator and navigator.userAgent compile", + symbol ?? null, + ); +} 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..6baeb65d1 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -46,7 +46,7 @@ export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Interface") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; return isReadlineModule(ambientModuleOf(d)); @@ -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; @@ -107,7 +107,7 @@ function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean { const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== "Readline") return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; const spec = ambientModuleOf(d); @@ -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/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts index b90eb4844..fc507e2bd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts @@ -11,11 +11,9 @@ export function isBuiltinClassInstance( const type = lowerer.checker.getTypeAtLocation(node); const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (symbol?.name !== name) return false; - return lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && - lowerer.isStdlibFile(declaration.getSourceFile()), - ); + return symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && + lowerer.isStdlibFile(declaration.getSourceFile()), + ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/reflection.ts b/packages/compiler/src/frontend/lowering/builtins/reflection.ts index 8b951e96c..1cf547be9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/reflection.ts +++ b/packages/compiler/src/frontend/lowering/builtins/reflection.ts @@ -23,13 +23,13 @@ export function lowerReflectCall( !call.questionDotToken && !access.questionDotToken && lowerer.stdlibGlobalMember(access, "Reflect") === "ownKeys" && - call.arguments.length === 1 && - !ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + !ts.isSpreadElement(call.arguments.nodes[0]!) ) { return { kind: "libCall", fn: "dyn.ownKeys", - args: [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)], + args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)], type: DYN, loc: locOf(call), }; @@ -39,9 +39,9 @@ export function lowerReflectCall( if (reflectMember === "get" || reflectMember === "set") { const required = reflectMember === "get" ? 2 : 3; if ( - call.arguments.length < required || - call.arguments.length > required + 1 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < required || + call.arguments.nodes.length > required + 1 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering( `Reflect.${reflectMember} with this argument shape`, @@ -50,9 +50,11 @@ export function lowerReflectCall( ); } const loc = locOf(call); - const args = call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)); const target = - call.arguments.length === required ? lowerer.declareHiddenLocal("%reflectTarget", DYN) : null; + call.arguments.nodes.length === required + ? lowerer.declareHiddenLocal("%reflectTarget", DYN) + : null; const operation: IrExpr = { kind: "libCall", fn: reflectMember === "get" ? "dyn.reflectGet" : "dyn.reflectSet", @@ -76,16 +78,16 @@ export function lowerReflectCall( const loc = locOf(call); const fenceHint = "the lowered form is Reflect.apply(path.join | path.resolve, , args) — call other functions directly (f(...args))"; - if (call.arguments.length !== 3 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering("Reflect.apply with this argument shape", call, fenceHint); } - const targetNode = call.arguments[0]!; + const targetNode = call.arguments.nodes[0]!; const bi = ts.isPropertyAccessExpression(targetNode) ? lowerer.builtinMemberOf(targetNode) : null; const fn = bi ? builtinModuleFnOf(lowerer, bi.module, bi.member) : null; if (!fn || fn.variadicPack !== true) { const target = lowerer.lowerExprExpecting(targetNode, DYN); - const receiver = lowerer.lowerExprExpecting(call.arguments[1]!, DYN); - const argumentsList = lowerer.lowerExprExpecting(call.arguments[2]!, DYN); + const receiver = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN); + const argumentsList = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN); return { kind: "libCall", fn: "dyn.reflectApply", @@ -94,7 +96,7 @@ export function lowerReflectCall( loc, }; } - const thisNode = call.arguments[1]!; + const thisNode = call.arguments.nodes[1]!; const effectFree = thisNode.kind === ts.SyntaxKind.ThisKeyword || ts.isIdentifier(thisNode) || @@ -107,6 +109,6 @@ export function lowerReflectCall( "the target ignores its receiver, so only effect-free spellings drop honestly (`this`, a binding, null, undefined)", ); } - const packed = lowerer.lowerExprExpecting(call.arguments[2]!, arrayOf(STRING)); + const packed = lowerer.lowerExprExpecting(call.arguments.nodes[2]!, arrayOf(STRING)); return { kind: "libCall", fn: fn.fn, args: [packed], type: fn.result, loc }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index b117c24ff..54ab22c05 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; -import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js"; +import { + BOOL, + DYN, + F64, + type IrExpr, + type IrStmt, + NULL_T, + STRING, + arrayOf, + bytesOf, + isUnitType, + typeEquals, +} from "../../../ir/ir.js"; /** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument * lowers as a number and packs into ONE f64[] array-literal argument @@ -17,6 +29,8 @@ export function lowerStringStaticCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken) return null; + const applied = lowerFromCharCodeApply(lowerer, call, access); + if (applied) return applied; const member = lowerer.stdlibGlobalMember(access, "String"); if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null; const loc = locOf(call); @@ -29,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; @@ -60,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; @@ -76,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, @@ -100,11 +117,104 @@ 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 }; } +/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom + * (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver + * is ignored, and a typed array or number array forwards to the packed + * fromCharCode like a spread. An untyped list reads a Uint8Array the same + * way at run time; any other value keeps Function.prototype.apply's + * generic array-like call. Null for every other call. */ +function lowerFromCharCodeApply( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "apply" || access.questionDotToken) return null; + const target = access.expression; + if ( + !ts.isPropertyAccessExpression(target) || + lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode" + ) { + return null; + } + 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); + if ( + listT === null || + (listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64))) + ) { + return null; + } + const loc = locOf(call); + 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); + const result: IrExpr = { + kind: "libCall", + fn: "string.fromCharCode", + args: [list], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; + } + const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN); + stmts.push({ + kind: "varDecl", + localId: listLocal.id, + init: lowerer.lowerExprExpecting(listNode, DYN), + loc, + }); + const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc }); + const bytes = bytesOf("u8"); + const generic: IrExpr = { + kind: "dynCheck", + value: { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(target, DYN), + method: "apply", + calleeName: "String.fromCharCode.apply", + args: [ + { + kind: "dynFrom", + value: { kind: "unitLit", unit: "null", type: NULL_T, loc }, + type: DYN, + loc, + }, + list(), + ], + type: DYN, + loc, + }, + type: STRING, + loc, + }; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc }, + then: { + kind: "libCall", + fn: "string.fromCharCode", + args: [{ kind: "dynCheck", value: list(), type: bytes, loc }], + type: STRING, + loc, + }, + else_: generic, + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + /** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16 * indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end; * MAX_SAFE_INTEGER has the same effect for every representable string. @@ -119,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, @@ -127,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..e89b1f07e 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) || @@ -361,13 +361,11 @@ export function storedTextCodecClassOf( ): TextCodecCtor["cls"] | null { const sym = lowerer.typeOf(expr).getSymbol(); if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + return sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ? sym.name : null; } @@ -380,16 +378,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 +404,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 +418,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 +540,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 +567,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 +579,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 +606,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 +650,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 +668,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..9f950cddc 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" @@ -155,9 +155,9 @@ function isTimerHandleTyped( const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getAliasSymbol() ?? t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker - .declarationsOf(sym) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())); + return sym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ); } function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean { return isTimerHandleTyped(lowerer, node, "Timeout"); @@ -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 new file mode 100644 index 000000000..2a9ff9eef --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -0,0 +1,130 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; +import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; + +/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, + * `sign`, `verify` and `digest` called on the crypto global (also through + * globalThis) lower to runtime entry points that take the argument vector + * as one dyn array and answer a promise, so Web IDL conversion, algorithm + * normalization and every error stay Node-shaped at run time. CryptoKey + * values are native handles; their type maps to dyn and their members + * are handle reads. Other SubtleCrypto methods are refused by name. */ + +const SUBTLE_LOWERED: ReadonlyMap = new Map([ + ["importKey", "webcrypto.importKey"], + ["sign", "webcrypto.sign"], + ["verify", "webcrypto.verify"], + ["digest", "webcrypto.digest"], +]); + +const SUBTLE_HINT = + "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + +/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isPropertyAccessExpression(node) && + !node.questionDotToken && + node.name.text === "subtle" && + stdlibGlobalNameOf(lowerer, node.expression) === "crypto" + ); +} + +/** `crypto.subtle.(...args)`: the lowered methods become their + * libCall over the argument vector; every other method is refused. */ +export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) { + return null; + } + const member = callee.name.text; + const fn = SUBTLE_LOWERED.get(member); + if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT); + if (callee.questionDotToken || call.questionDotToken) { + lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT); + } + 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.nodes[0]; + if ( + member === "importKey" && + format !== undefined && + ts.isStringLiteralLike(format) && + format.text === "jwk" + ) { + lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); + } + const loc = locOf(call); + const args = call.arguments.nodes.map((arg) => + ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + return { + kind: "libCall", + fn, + args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }], + type: { kind: "promise", inner: member === "verify" ? BOOL : DYN }, + loc, + }; +} + +/** `v instanceof CryptoKey` against the global. */ +export function lowerCryptoKeyInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null; + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "webcrypto.isCryptoKey", + args: [value], + type: BOOL, + loc: locOf(expr), + }; +} + +/** TypeScript reads of `crypto.subtle` and its members outside a direct + * call: the global's dynamic object does not carry the lowered methods, + * so a detached read is refused rather than answering a value without + * them. JavaScript keeps the dynamic object's reads. */ +export function fenceWebCryptoMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression, + use: "read" | "call", +): IrExpr | null { + if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null; + if (isJsSourceFile(access.getSourceFile())) return null; + if (isSubtleReceiver(lowerer, access)) { + lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT); + } + if (isSubtleReceiver(lowerer, access.expression)) { + lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT); + } + return null; +} + +/** Whether a checker type is one of the WebCrypto interfaces reached + * through the crypto global's members (SubtleCrypto, CryptoKey and the + * algorithm dictionaries). While `crypto` was typed `unknown` those member + * reads were untyped, so JavaScript destructurings of them keep the + * module-global path untyped bindings take. `Crypto` itself is excluded: + * the global was `unknown`, which already mapped to dyn. */ +export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean { + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + return ( + symbol !== undefined && + symbol.name !== "Crypto" && + WEBCRYPTO_DYN_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 2311e0f96..335fdb969 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; @@ -175,7 +175,7 @@ function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: strin const t = lowerer.checker.getTypeAtLocation(node); const sym = t.getSymbol(); if (sym?.name !== name) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let up: ts.Node | undefined = d.parent; @@ -194,7 +194,7 @@ export function lowerMessageChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "MessageChannel")) return null; - if ((expression.arguments ?? []).length > 0) { + if ((expression.arguments?.nodes ?? []).length > 0) { lowerer.noLowering( "MessageChannel constructor arguments", expression, @@ -214,7 +214,7 @@ export function lowerMessageChannelNew( /** receiveMessageOnPort(port): { message } or undefined. */ export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`receiveMessageOnPort with ${args.length} arguments`, call); } @@ -244,7 +244,7 @@ export function lowerBroadcastChannelNew( expression: ts.NewExpression, ): IrExpr | null { if (!isWorkerThreadsClass(lowerer, expression, "BroadcastChannel")) return null; - const args = expression.arguments ?? []; + const args = expression.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `BroadcastChannel constructor with ${args.length} arguments`, @@ -296,7 +296,7 @@ export function lowerEnvironmentData( call: ts.CallExpression, member: "setEnvironmentData" | "getEnvironmentData", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; const arity = member === "setEnvironmentData" ? 2 : 1; if (args.length > arity || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); @@ -331,7 +331,7 @@ export function lowerEnvironmentData( /** markAsUncloneable(value): structuredClone and postMessage then throw * DataCloneError for the value (scr_json.c). It needs no worker runtime. */ export function lowerUncloneable(lowerer: Lowerer, call: ts.CallExpression): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`markAsUncloneable with ${args.length} arguments`, call); } @@ -354,7 +354,7 @@ export function lowerUntransferable( call: ts.CallExpression, member: "markAsUntransferable" | "isMarkedAsUntransferable", ): IrExpr { - const args = call.arguments; + const args = call.arguments.nodes; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering(`${member} with ${args.length} arguments`, call); } diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 3119fa2ab..f5600b033 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -504,9 +504,9 @@ export function completeFuncValueArgs( mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required", })); shapes.push({ type: rest, mode: "rest" }); - return completeArgs(lowerer, call.arguments, shapes, loc, call); + return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call); } - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => lowerer.lowerExprExpecting(arg, funcType.params[index]), ); for (let i = args.length; i < funcType.params.length; i++) { @@ -553,7 +553,7 @@ export function lowerSpreadArgsCall( !(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined) ) return null; - if (!expr.arguments.some((a) => ts.isSpreadElement(a))) return null; + if (!expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) return null; if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null; const calleeName = ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression) @@ -563,7 +563,7 @@ export function lowerSpreadArgsCall( : "value"; // Lower every argument ONCE, in source order (the IR nests them in // exactly this order, so runtime evaluation order is JS's). - const parts = expr.arguments.map((a) => + const parts = expr.arguments.nodes.map((a) => ts.isSpreadElement(a) ? { spreadOf: a.expression, node: null, v: lowerer.lowerExpr(a.expression) } : { spreadOf: null, node: a as ts.Expression, v: lowerer.lowerExpr(a) }, diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9375576f7..6db1735db 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -189,7 +189,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa executor && (ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) && ts.isNewExpression(construction) && - construction.arguments?.[0] === executor && + construction.arguments?.nodes[0] === executor && lowerer.isStdlibGlobal(construction.expression, "Promise"); if ( !nativeExecutor && @@ -408,7 +408,15 @@ export function paramShapes( return { type: mapped, mode: "rest" }; } if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped); - if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) { + // A checked-value slot holds undefined directly, so an omitted + // argument needs no union arm (paramShape's declared-signature rule; + // an erased type parameter and a class-implemented interface view + // both map here). + if ( + declParam.questionToken && + !lowerer.bareUndefinedArmedUnion(mapped) && + mapped.kind !== "dyn" + ) { lowerer.unsupported( "SC1090", declParam, @@ -634,10 +642,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // body (run()/discover() skip by the same test). Generic mixins still // register their generic signature below: non-mixin-shaped calls // degrade to the generic machinery's own per-site fences. - if (!decl.typeParameters && mixinFnShapeOf(lowerer, decl)) return; - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + if (!decl.typeParameters?.nodes && mixinFnShapeOf(lowerer, decl)) return; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = decl.asteriskToken !== undefined; - if (decl.typeParameters) { + if (decl.typeParameters?.nodes) { // Generic async composes: each monomorphized instance is an async // IrFunction like any other — its own spawn wrapper, its body // returning the resolved promise's inner (lowerGenericInstance). @@ -669,14 +677,14 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara } } - const params = lowerer.paramShapes(decl.parameters); + const params = lowerer.paramShapes(decl.parameters.nodes); // The zero-parameter declaration retains its dynRest ABI; value // functions use the full arguments slot even with no named parameters. appendArgumentsParameter( lowerer, decl, params, - decl.parameters.length > 0 ? "arguments" : "dynRest", + decl.parameters.nodes.length > 0 ? "arguments" : "dynRest", ); const nameBlame: ts.Node = decl.name ?? decl; const returnType = lowerer.declaredReturnType(decl, nameBlame); @@ -728,7 +736,7 @@ function appendArgumentsParameter( !bodyReadsArguments(node) ) return; - if (node.parameters.length > 0 || ts.isMethodDeclaration(node)) { + if (node.parameters.nodes.length > 0 || ts.isMethodDeclaration(node)) { const classMethod = ts.isMethodDeclaration(node) && (ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent)); @@ -765,7 +773,9 @@ export function lambdaSignature( isJsSourceFile(node.getSourceFile()) && !node.type && (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) && - node.parameters.every((param) => !param.type && !param.dotDotDotToken && !param.initializer) && + node.parameters.nodes.every( + (param) => !param.type && !param.dotDotDotToken && !param.initializer, + ) && ts.isBinaryExpression(node.parent) && node.parent.right === node && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && @@ -775,11 +785,14 @@ export function lambdaSignature( ) { const receiver = lowerer.lowerExpr(node.parent.left.expression); if (receiver.type.kind === "dyn") { - const shapes: ParamShape[] = node.parameters.map(() => ({ type: DYN, mode: "required" })); + const shapes: ParamShape[] = node.parameters.nodes.map(() => ({ + type: DYN, + mode: "required", + })); return { shapes, funcType: funcTypeFromParamShapes(shapes, DYN) }; } } - if (node.typeParameters && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes && !lowerer.erasingLambdas.has(node)) { // Generic function-like forms monomorphize only where a static home // exists: top-level generic function declarations, generic methods // (class and object-literal), and module-scope never-reassigned @@ -797,14 +810,14 @@ export function lambdaSignature( : "generic arrow/function expressions outside a never-reassigned module-scope binding (only `const f = (x: T) => ...` bindings and top-level generic function declarations monomorphize)", ); } - const shapes = lowerer.paramShapes(node.parameters); + const shapes = lowerer.paramShapes(node.parameters.nodes); // Error-first callbacks captured by native filesystem adapters receive // the selected runtime overload. Ambient contextual inference can pick // the string overload even when options request a Buffer. if ( (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isCallExpression(node.parent) && - node.parent.arguments.at(-1) === node + node.parent.arguments.nodes.at(-1) === node ) { const builtin = ts.isPropertyAccessExpression(node.parent.expression) ? lowerer.builtinMemberOf(node.parent.expression) @@ -817,7 +830,7 @@ export function lambdaSignature( builtinModuleFnOf(lowerer, builtin.module, builtin.member)?.fn === "fs.callbackCall" ) { for (let index = 0; index < shapes.length; index++) { - if (!node.parameters[index]?.type) { + if (!node.parameters.nodes[index]?.type) { shapes[index]!.type = DYN; if (shapes[index]!.bodyType) shapes[index]!.bodyType = DYN; } @@ -854,7 +867,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 @@ -873,8 +886,7 @@ export function lambdaSignature( const ctxSigs = ctxType ? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType)) : []; - const ctxDecl = - ctxSigs.length === 1 ? lowerer.checker.signatureDeclaration(ctxSigs[0]!) : undefined; + const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined; const ctxRetRaw = ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile ? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!)) diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index d72792ae7..5ea4001fc 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -37,7 +37,7 @@ export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): b if (!ts.isIdentifier(node) || node.text !== "exports") return false; if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; return !decls.some( (d) => !d.getSourceFile().isDeclarationFile && @@ -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; @@ -349,7 +349,7 @@ function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { /** A function declaration, or a variable initialized with a function or * arrow expression, that never reads `this`. */ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const fn = decls.find(ts.isFunctionDeclaration); if (fn !== undefined) return !functionUsesThis(fn); const v = decls.find(ts.isVariableDeclaration); @@ -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..174f9dc7c 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 = @@ -353,7 +353,7 @@ export function cjsExportValueByName( if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); const direct = lowerer.globalsBySymbol.get(sym); if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { const byDecl = lowerer.globalsByDeclNode.get(d); if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; // Alias plumbing: a shorthand/identifier table entry or a member diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index f7955fc23..cd83c8709 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") ?? @@ -132,7 +132,7 @@ export function tscInteropBindingOf( ): TscInteropBinding | null { if (!ts.isIdentifier(expr)) return null; const sym = lowerer.checker.getSymbolAtLocation(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + const decl = sym ? sym.declarations.find(ts.isVariableDeclaration) : undefined; return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; } @@ -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 a0ed496e1..4d0bed916 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2883,7 +2883,7 @@ export class ClassDynamicDispatch { if (info.symbolMethods) for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]); for (const [symbol, name] of symbolEntries) { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && 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..c07ec9136 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -93,7 +93,7 @@ export function collectClassMethodMutations( value = unwrap(value); if (ts.isIdentifier(value)) { const symbol = lowerer.checker.getSymbolAtLocation(value); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -162,7 +162,7 @@ export function collectClassMethodMutations( const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => { if (!ts.isIdentifier(key)) return null; const symbol = lowerer.checker.getSymbolAtLocation(key); - const decl = symbol && lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol && symbol.declarations[0]; if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; if (!ts.isVariableDeclarationList(list) || !(list.flags & ts.NodeFlags.Const)) return null; @@ -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) && @@ -413,7 +413,7 @@ export function collectClassMethodMutations( const symbol = ts.isPropertyAccessExpression(access) ? lowerer.checker.getSymbolAtLocation(access.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name); - if (symbol && lowerer.checker.declarationsOf(symbol).some(ts.isMethodDeclaration)) + if (symbol && symbol.declarations.some(ts.isMethodDeclaration)) lowerer.prototypeMethodAccesses.set(name, access); } lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]); @@ -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) && @@ -480,9 +480,8 @@ export function collectClassMethodMutations( } const type = lowerer.checker.getDeclaredTypeOfSymbol(symbol); 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 (!property || !property.declarations.some(ts.isMethodDeclaration)) continue; + if (cls.typeParameters !== undefined && cls.typeParameters.nodes.length > 0) { owners = null; break; } @@ -517,7 +516,9 @@ export function collectClassMethodMutations( ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ) { - if (member.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)) + if ( + member.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) + ) return null; } else return null; const symbol = lowerer.checker.getSymbolAtLocation(cls.name); diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 50278c3b7..10572377e 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -348,7 +348,7 @@ function reflectedMethodValue( if (existing) return existing; const declaration = info.decl; if (!declaration) return null; - const member = declaration.members.find((member) => { + const member = declaration.members.nodes.find((member) => { if ( (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.name || diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 2f66a5203..0c9a24f24 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -99,7 +99,7 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean { if (!symbol) return false; const cached = lowerer.protocolOwnedBindings.get(symbol); if (cached !== undefined) return cached; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; let owned = decl !== undefined && decl.getSourceFile() === id.getSourceFile() && @@ -191,7 +191,7 @@ function protocolSignature( const recvTs = lowerer.typeOf(access.expression); const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if (!property) return null; - const declarations = lowerer.checker.declarationsOf(property); + const declarations = property.declarations; if ( declarations.length === 0 || !declarations.every( @@ -200,19 +200,20 @@ function protocolSignature( ) return null; const signature = lowerer.checker.getResolvedSignature(call); - const declaration = signature && lowerer.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; if ( !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) { @@ -489,7 +490,7 @@ export function lowerProtocolPropertyRead( if (!lowerer.classImplementedProtocol(receiverType)) return null; const name = access.name.text; const property = lowerer.checker.getPropertyOfType(receiverType, name); - const declarations = property ? lowerer.checker.declarationsOf(property) : []; + const declarations = property ? property.declarations : []; if (declarations.length === 0 || !declarations.every((decl) => ts.isPropertySignature(decl))) return null; const result = lowerer.mapTypeOf(lowerer.typeOf(access)); diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 3c610c51a..5a27f0815 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -29,22 +29,21 @@ 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) : []) { if ( - !this.checker - .declarationsOf(property) - .some( - (member) => - ts.isMethodDeclaration(member) && - !member.modifiers?.some( - (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, - ), - ) + !property.declarations.some( + (member) => + ts.isMethodDeclaration(member) && + !member.modifiers?.nodes.some( + (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword, + ), + ) ) continue; const entries = this.methods.get(property.name) ?? []; @@ -54,7 +53,7 @@ export class ClassProtocols { } if ( ts.isObjectLiteralExpression(node) && - node.properties.some( + node.properties.nodes.some( (property) => (ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property) || @@ -167,14 +166,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 +203,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 +212,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 +234,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); @@ -287,7 +287,7 @@ export class ClassProtocols { for (const part of parts) { if ((part.flags & ts.TypeFlags.TypeParameter) === 0) continue; const symbol = part.getSymbol(); - for (const declaration of symbol ? this.checker.declarationsOf(symbol) : []) + for (const declaration of symbol ? symbol.declarations : []) if (ts.isTypeParameterDeclaration(declaration) && declaration.parent !== undefined) facts.genericObservers.add(declaration.parent); } @@ -499,7 +499,7 @@ export class ClassProtocols { if (context !== undefined && this.acceptsAnyValue(context)) return true; if (facts.genericObservers.size === 0) return false; const signature = this.checker.getResolvedSignature(arg.call); - const declaration = signature && this.checker.signatureDeclaration(signature); + const declaration = signature && signature.declaration; return ( declaration !== undefined && (facts.genericObservers.has(declaration) || @@ -518,13 +518,10 @@ export class ClassProtocols { .some( (property) => names.has(property.name) && - this.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration), - ), + property.declarations.some( + (declaration) => + ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration), + ), ); } @@ -578,9 +575,7 @@ export class ClassProtocols { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - this.checker - .declarationsOf(symbol) - .some((declaration) => !declaration.getSourceFile().isDeclarationFile) && + symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) && // `typeof C` (the class value) shares the class symbol. this.checker.getConstructSignatures(type).length === 0 ); @@ -601,13 +596,10 @@ export class ClassProtocols { // A program interface a class matches by its methods (protocol below). return ( this.methods.size > 0 && - this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) && + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) && this.protocol(type) ); } @@ -622,7 +614,7 @@ export class ClassProtocols { if (this.checker.getConstructSignatures(type).length > 0) return false; const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; return ( declarations.length > 0 && declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile) @@ -710,13 +702,10 @@ export class ClassProtocols { if (symbol && protocols.has(symbol)) return true; if ( !symbol || - !this.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !declaration.getSourceFile().isDeclarationFile, - ) + !symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile, + ) ) return false; for (const property of properties) { diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 1db709e91..2787c9021 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", @@ -553,7 +555,7 @@ function prototypeProtectedKeys( } else if (name.startsWith("%symbol:")) { const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : []; const entry = entries.find(([, field]) => field === name); - const declaration = entry ? lowerer.checker.valueDeclarationOf(entry[0]) : undefined; + const declaration = entry ? entry[0].valueDeclaration : undefined; if ( !declaration || !ts.isVariableDeclaration(declaration) || @@ -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-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index c172b40cf..f52026ba4 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,8 +1,115 @@ -import { BOOL, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { checkedClassAssertion } from "./class-assertions.js"; import type { Lowerer } from "./lowerer.js"; +/** The classes a narrowed read of a `staticClass` value can hold when the + * checker typed it as the subclass `narrowed`, as a class union, or null + * when `narrowed` is the only one. + * + * TypeScript reduces a union of type-parameter intersections by structural + * subtyping. With ``, `node instanceof Identifier || node + * instanceof PrivateIdentifier` narrows `node` to `T & PrivateIdentifier` + * when Identifier has every member PrivateIdentifier has, so the narrowed + * read can hold an Identifier. Class identity is nominal in compiled code: + * such a read holds the narrowed class or any class under `staticClass` + * that is assignable to it without extending it. A narrowing outside a + * type-parameter intersection keeps every class it names. */ +export function structuralNarrowing( + lowerer: Lowerer, + node: ts.Node, + staticClass: string, + narrowed: IrType & { kind: "object" }, +): (IrType & { kind: "union" }) | null { + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const key = `${staticClass}:${narrowed.className}`; + let peers = lowerer.structuralClassPeers.get(key); + if (peers === undefined) { + peers = assignableClassPeers(lowerer, staticClass, narrowed.className); + lowerer.structuralClassPeers.set(key, peers); + } + if (peers.length === 0) return null; + const arms: IrType[] = [ + narrowed, + ...peers.map((className) => ({ kind: "object" as const, className })), + ]; + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: lowerer.unions.intern(arms) }; +} + +/** The type a member access dispatches on when its receiver `node` is a + * narrowed read of a type-parameter intersection, or null to use the + * checker's type: the class union structuralNarrowing reads, or the + * receiver's own class when this instantiation's class is unrelated to the + * narrowed one but assignable to it (with `T = Identifier`, the reduced + * `T & PrivateIdentifier` still holds an Identifier). */ +export function structuralReadType(lowerer: Lowerer, node: ts.Expression): IrType | null { + if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; + if (!intersectsTypeParameter(lowerer.checker.getTypeAtLocation(node))) return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (narrowed?.kind !== "object") return null; + const stored = ts.isIdentifier(node) ? lowerer.peekLocal(node)?.type : undefined; + const symbol = lowerer.checker.getSymbolAtLocation(ts.isIdentifier(node) ? node : node.name); + const declared = + stored ?? (symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null); + if (declared?.kind !== "object" || declared.className === narrowed.className) return null; + if (lowerer.isSubclassOf(narrowed.className, declared.className)) + return structuralNarrowing(lowerer, node, declared.className, narrowed); + if (lowerer.isSubclassOf(declared.className, narrowed.className)) return null; + const declaredType = classInstanceType(lowerer, declared.className); + const narrowedType = classInstanceType(lowerer, narrowed.className); + return declaredType !== null && + narrowedType !== null && + lowerer.checker.isTypeAssignableTo(declaredType, narrowedType) + ? declared + : null; +} + +function classInstanceType(lowerer: Lowerer, className: string): ts.Type | null { + const decl = lowerer.classes.get(className)?.decl; + if (!decl?.name || decl.typeParameters?.nodes.length) return null; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; +} + +function intersectsTypeParameter(type: ts.Type): boolean { + try { + if (type.isUnionType()) return ts.constituentTypes(type).some(intersectsTypeParameter); + return ( + (type.flags & ts.TypeFlags.Intersection) !== 0 && + ts.constituentTypes(type).some((part) => (part.flags & ts.TypeFlags.TypeParameter) !== 0) + ); + } catch (e) { + if (ts.isUnreadableConstituentsPanic(e)) return false; + throw e; + } +} + +/** The outermost classes under `staticClass`, other than `target` and its + * subclasses, whose instance type is assignable to `target`'s. */ +function assignableClassPeers(lowerer: Lowerer, staticClass: string, target: string): string[] { + const targetType = classInstanceType(lowerer, target); + if (targetType === null) return []; + const assignable: string[] = []; + for (const className of lowerer.classes.keys()) { + if ( + className === target || + !lowerer.isSubclassOf(className, staticClass) || + lowerer.isSubclassOf(className, target) + ) + continue; + const type = classInstanceType(lowerer, className); + if (type !== null && lowerer.checker.isTypeAssignableTo(type, targetType)) + assignable.push(className); + } + // A subclass of an assignable class is one of its instances already. + return assignable.filter( + (className) => + !assignable.some((other) => other !== className && lowerer.isSubclassOf(className, other)), + ); +} + /** A base-class slot stays one pointer after a guard selects several * subclasses. Recover the tagged view from runtime membership without * copying the object or guessing which of the remaining layouts it has. */ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts index 1ff401b4e..2bc991c14 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts @@ -1,4 +1,4 @@ -import { planRecordUnionWrap } from "../union-retag.js"; +import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js"; import type { WidthLift } from "../width-lift.js"; import type { IrType } from "../../../ir/ir.js"; import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js"; @@ -78,6 +78,24 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width }); if (absentOnly.length === 1) selected = absentOnly; } + // A record that fits several arms of a discriminated union selects one + // by its discriminant at run time. Without this, an arm that a nested + // field could reach only ambiguously dropped out of the candidates, and + // an arm reached by dropping fields could win alone. + if (selected.length !== 1 && src.kind === "record") { + const source = lowerer.shapes.get(src.shapeId); + if ( + source && + planRecordUnionLift( + source, + def, + (id) => lowerer.shapes.get(id), + (from, to) => lowerer.widthLiftPlan(from, to), + ) + ) { + return { how: "discriminantRoute" }; + } + } if (selected.length !== 1) return null; return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm }; } @@ -407,7 +425,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/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 925f59d9b..e80b3401a 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -106,6 +106,16 @@ export function applyWidthLift( ); return { kind: "call", callee: helper, args: [value], type: dst, loc }; } + case "discriminantRoute": { + if (dst.kind !== "union" || value.type.kind !== "record") + throw new InternalCompilerError("lowerer bug: record discriminant route shape"); + const helper = lowerer.recordUnionRouteHelper(value.type, dst.unionId, loc); + if (!helper) + throw new InternalCompilerError( + "lowerer bug: planned record discriminant route failed to intern", + ); + return { kind: "call", callee: helper, args: [value], type: dst, loc }; + } case "retag": { if (dst.kind !== "union" || value.type.kind !== "union") throw new InternalCompilerError("lowerer bug: retag lift shape"); @@ -603,6 +613,10 @@ function buildRecordProjection( const localId = parameter + ".0"; const receiver = varRef(localId, source, loc); const result: IrType = { kind: "record", shapeId: target.id }; + // A record source's copy keeps the source's key order; a class + // instance's fields keep today's table. + const copyOf = (node: IrExpr): IrExpr => + source.kind === "record" ? lowerer.keyOrder.copy(node, source.shapeId) : node; return { name, params: [{ localId, name: parameter, type: source }], @@ -611,7 +625,7 @@ function buildRecordProjection( body: [ { kind: "return", - value: { + value: copyOf({ kind: "recordLit", fields: target.fields.map((field) => { const conversion = plan.get(field.name)!; @@ -661,7 +675,7 @@ function buildRecordProjection( }), type: result, loc, - }, + }), loc, }, ], diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 0356b714e..06c0af9c6 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -4,6 +4,7 @@ import { nodeThrowExpr } from "../../../ir/build.js"; import { planUnionRetag, buildUnionRetag, + planRecordUnionLift, planRecordUnionWrap, buildRecordUnionWrap, } from "../union-retag.js"; @@ -284,6 +285,38 @@ export function recordUnionWrapHelper( return name; } +/** Interned `%record.route.(r)`: a record that fits several arms of a + * discriminated union, converted into the arm its discriminant names + * (planRecordUnionLift). The width plan's `discriminantRoute` lift. */ +export function recordUnionRouteHelper( + lowerer: Lowerer, + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, +): string | null { + const shape = lowerer.shapes.get(source.shapeId); + const to = lowerer.unions.get(toId); + if (!shape || !to) return null; + const key = `recordRoute:${source.shapeId}:${toId}`; + const existing = lowerer.coercions.retags.get(key); + if (existing) return existing; + const plan = planRecordUnionLift( + shape, + to, + (id) => lowerer.shapes.get(id), + (from, dst) => lowerer.widthLiftPlan(from, dst), + ); + if (!plan) return null; + const name = `%record.route.${lowerer.coercions.retags.size}`; + lowerer.coercions.retags.set(key, name); + lowerer.liftedFns.push( + buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => + lowerer.applyWidthLift(lift, value, dst, loc), + ), + ); + return name; +} + export function unionRetagHelper( lowerer: Lowerer, fromId: string, 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/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts index 57b75daf5..29b358e99 100644 --- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -13,12 +13,7 @@ export function concreteCollectionNarrow( const symbol = narrowed.getSymbol(); const name = symbol?.name; if (name !== "Map" && name !== "Set") return null; - if ( - !symbol || - !lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) - ) + if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))) return null; const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts index e2bd82516..77f72dc15 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts @@ -13,8 +13,8 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo }); if (!decl) throw new Error("fixture has no class"); const cls = decl; - const ctor = cls.members.find(ts.isConstructorDeclaration) ?? null; - const fields = cls.members + const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null; + const fields = cls.members.nodes .filter(ts.isPropertyDeclaration) .map((member) => ({ name: member.name.getText(), initializer: member.initializer })); const fieldNames = new Set(fields.map((field) => field.name)); @@ -28,7 +28,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo privateMethodBody: (call) => { if (!ts.isPropertyAccessExpression(call.expression)) return null; const name = call.expression.name.text; - const method = cls.members.find( + const method = cls.members.nodes.find( (member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && member.name.getText() === name, ); diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index 484920a1c..b9c8d7e07 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -89,10 +89,11 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) return true; if (ts.isCallExpression(node) && thisMember(node.expression)) { - if (node.arguments.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) return true; + if (node.arguments.nodes.some((arg) => mentionsReceiver(arg) && exposes(arg, depth))) + return true; const body = depth < HELPER_DEPTH ? inputs.privateMethodBody(node) : null; if (!body) return true; - return !followStatements(body.statements, depth + 1); + return !followStatements(body.statements.nodes, depth + 1); } if (thisMember(node)) { const name = node.name.text; @@ -137,7 +138,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca } return true; }; - const statements = inputs.ctor?.body?.statements ?? []; + const statements = inputs.ctor?.body?.statements?.nodes ?? []; let index = 0; if (inputs.derived) { // Own initializers run when super() returns. A base construction that @@ -149,7 +150,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca ts.isCallExpression(stmt.expression) && stmt.expression.expression.kind === ts.SyntaxKind.SuperKeyword; if (isSuper) { - if (stmt.expression.arguments.some((arg) => mentionsReceiver(arg))) return stop(); + if (stmt.expression.arguments.nodes.some((arg) => mentionsReceiver(arg))) return stop(); break; } // super() nested inside other statements: keep the analysis simple. diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts index b88de3795..a22a82bb1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts @@ -16,7 +16,7 @@ export function lowerArrayCallbackCall( access: ts.ElementAccessExpression, array: IrExpr, ): IrExpr | null { - if (array.type.kind !== "array" || call.arguments.some(ts.isSpreadElement)) return null; + if (array.type.kind !== "array" || call.arguments.nodes.some(ts.isSpreadElement)) return null; const elem = array.type.elem; const arms = elem.kind === "union" ? (lowerer.unions.get(elem.unionId)?.arms ?? []) : [elem]; const functions = arms.filter((arm) => arm.kind === "func"); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index a79864983..2e93e2fd5 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -74,7 +74,7 @@ export function lowerArrayHofCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const booleanFilter = method === "filter" && @@ -341,7 +341,7 @@ function lowerFastCallback( // The optional-read prepass binds an unannotated element parameter to // the array's runtime value type. Rebind it to the plain element for // this lowering only. - const param = argNode.parameters[elemIndex]; + const param = argNode.parameters.nodes[elemIndex]; if (!param || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name)) return null; const symbol = param.type ? undefined : lowerer.checker.getSymbolAtLocation(param.name); @@ -563,7 +563,7 @@ export function lowerArrayUnionHofCall( if (!lowerer.isStdlibMember(access)) return null; const def = lowerer.unions.get(receiverT.unionId); if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "array")) return null; - if (call.arguments.length !== 1 || !call.arguments[0]) return null; + if (call.arguments.nodes.length !== 1 || !call.arguments.nodes[0]) return null; const arrays = def.arms as (IrType & { kind: "array" })[]; const valueArms: IrType[] = []; const addValueArm = (type: IrType): boolean => { @@ -586,7 +586,7 @@ export function lowerArrayUnionHofCall( valueArms.length === 1 ? valueArms[0]! : { kind: "union", unionId: lowerer.unions.intern(valueArms) }; - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [valueT], receiverT); const fnRet = fnArg.type.ret; if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) { @@ -1025,7 +1025,7 @@ export function lowerArrayFindLikeCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded // find's result must be reachable from the element itself; a predicate // that narrows to another layout keeps the optional element visit. @@ -1495,7 +1495,7 @@ export function lowerArrayFlatMapCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded const present = presentElements(lowerer, call, elem); const { fnArg, arity } = lowerArrayCallback( @@ -1779,9 +1779,9 @@ export function lowerArrayReduceCall( const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); const arrT = arrayOf(elem); - const argNode = call.arguments[0]; + const argNode = call.arguments.nodes[0]; if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded - const hasInit = call.arguments.length === 2; + const hasInit = call.arguments.nodes.length === 2; const accT = lowerer.runtimeOptionalReduceTypes.get(call) ?? (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem)); @@ -1818,7 +1818,7 @@ export function lowerArrayReduceCall( fast?.type ?? null, ); const args: IrExpr[] = [receiver, fnArg]; - if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, accT)); + if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, accT)); // The fast closure is created last; making a closure has no observable // effect, so the initial value still evaluates right after the callback. if (fast) args.push(fast); @@ -2037,7 +2037,7 @@ export function lowerArraySortCall( const method = access.name.text === "toSorted" ? "toSorted" : "sort"; const copyFirst = method === "toSorted"; if ( - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && (elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool") ) { return { @@ -2049,15 +2049,15 @@ export function lowerArraySortCall( loc, }; } - if (call.arguments.length !== 1) { + if (call.arguments.nodes.length !== 1) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "default ordering supports string[], number[] and boolean[]; other element types need a comparator", ); } const receiver = lowerer.lowerExpr(access.expression); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, argNode); let unwrappedArg = argNode; while ( @@ -2205,8 +2205,8 @@ export function lowerDynArrayFilterCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); // The result element type, from what the call flows INTO: the // contextual array (or the single array arm of a contextual union — @@ -2270,8 +2270,8 @@ export function lowerDynArrayFlatMapCall( access: ts.PropertyAccessExpression, recv: IrExpr, ): IrExpr | null { - if (call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; const loc = locOf(call); const { fnArg, arity } = lowerArrayCallback(lowerer, argNode, [DYN], DYN); const ret = fnArg.type.ret; @@ -2631,7 +2631,7 @@ export function lowerTuplePredicateCall( reads: IrExpr[], ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; const booleanFilter = method === "filter" && ts.isIdentifier(argNode) && diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 57b8caf14..4f6e4ca43 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -144,7 +144,7 @@ export function arrayConcatHelper( }, ...append(varRef("a.0", arrT, loc), elem, node.expression), ...shape.flatMap((type, i) => - append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments[i]!), + append(varRef(`x.${i}`, type ? arrayOf(type) : elem, loc), type, node.arguments.nodes[i]!), ), { kind: "return", value: out, loc }, ]; diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 015cb2123..6072b58b7 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -448,12 +448,12 @@ export function lowerArrayOfCall( if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { lowerer.badType(call, lowerer.typeOf(call)); } - if (call.arguments.some(ts.isSpreadElement)) { - return lowerArraySpreadItems(lowerer, call.arguments, result.elem, result, locOf(call)); + if (call.arguments.nodes.some(ts.isSpreadElement)) { + return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call)); } return { kind: "arrayLit", - elems: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), + elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)), type: result, loc: locOf(call), }; @@ -795,7 +795,7 @@ export function lowerArrayFromCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "from") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if ( isJsSourceFile(call.getSourceFile()) && args.length >= 1 && @@ -884,9 +884,9 @@ export function lowerArrayFromCall( if ( args.length === 1 && ts.isObjectLiteralExpression(args[0]!) && - args[0]!.properties.length === 1 + args[0]!.properties.nodes.length === 1 ) { - const n = lowerLengthProp(lowerer, args[0]!.properties[0]!); + const n = lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!); if (n) { if (n.type.kind !== "f64") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); const arrT = lowerer.mapTypeOf(lowerer.typeOf(call)); @@ -941,7 +941,7 @@ export function lowerArrayFromCall( const valueT = arrayValueType(lowerer, arrT.elem); const firstParam = ts.isArrowFunction(mapper) || ts.isFunctionExpression(mapper) - ? mapper.parameters[0]?.name + ? mapper.parameters.nodes[0]?.name : undefined; const paramSymbol = firstParam && ts.isIdentifier(firstParam) @@ -1021,8 +1021,10 @@ export function lowerArrayFromCall( ); } const n = - args.length === 2 && ts.isObjectLiteralExpression(args[0]!) && args[0]!.properties.length === 1 - ? lowerLengthProp(lowerer, args[0]!.properties[0]!) + args.length === 2 && + ts.isObjectLiteralExpression(args[0]!) && + args[0]!.properties.nodes.length === 1 + ? lowerLengthProp(lowerer, args[0]!.properties.nodes[0]!) : null; if (!n) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts index db60cdf1b..7aeec62ed 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-from-async.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-from-async.ts @@ -43,7 +43,7 @@ export function lowerArrayFromAsyncCall( if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null; if (access.name.text !== "fromAsync") return null; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `Array.fromAsync with ${args.length} arguments`, diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 40c2e730d..7ef96457c 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -318,7 +318,8 @@ export function lowerArrayMethodCall( if (lowerer.chainBlocked(access, call)) return null; const name = access.name.text; const iteratorMethod = - (name === "values" || name === "keys" || name === "entries") && call.arguments.length === 0; + (name === "values" || name === "keys" || name === "entries") && + call.arguments.nodes.length === 0; if ( !ARRAY_METHODS.has(name) && name !== "sort" && @@ -446,8 +447,8 @@ export function lowerArrayMethodCall( return lowerArraySortCall(lowerer, call, access, elem, receiverIr); } if (name === "toReversed") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.toReversed with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.toReversed with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -459,8 +460,8 @@ export function lowerArrayMethodCall( }; } if (name === "reverse") { - if (call.arguments.length !== 0) { - lowerer.noLowering(`.reverse with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`.reverse with ${call.arguments.nodes.length} arguments`, call); } return { kind: "arrIntrinsic", @@ -472,10 +473,10 @@ export function lowerArrayMethodCall( }; } if (name === "fill") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.fill with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.fill with ${call.arguments.nodes.length} arguments`, call); } - const value = call.arguments[0] ? lowerer.lowerExpr(call.arguments[0]) : null; + const value = call.arguments.nodes[0] ? lowerer.lowerExpr(call.arguments.nodes[0]) : null; if ( value && value.type.kind === "dyn" && @@ -485,7 +486,7 @@ export function lowerArrayMethodCall( ) { lowerer.noLowering( ".fill of a checked-dynamic reference into a static array", - call.arguments[0]!, + call.arguments.nodes[0]!, ); } const writeUndefined = @@ -495,35 +496,53 @@ export function lowerArrayMethodCall( lowerer.lowerExpr(access.expression), value, writeUndefined, - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array fill start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array fill end"), + lowerArrayPosition(lowerer, call.arguments.nodes[1], numLit(0, loc), "array fill start"), + lowerArrayPosition(lowerer, call.arguments.nodes[2], numLit(Infinity, loc), "array fill end"), receiverIr, loc, ); } if (name === "copyWithin") { - if (call.arguments.length > 3 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.copyWithin with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.copyWithin with ${call.arguments.nodes.length} arguments`, call); } return lowerArrayCopyWithin( lowerer.lowerExpr(access.expression), - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array copyWithin target"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(0, loc), "array copyWithin start"), - lowerArrayPosition(lowerer, call.arguments[2], numLit(Infinity, loc), "array copyWithin end"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[0], + numLit(0, loc), + "array copyWithin target", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(0, loc), + "array copyWithin start", + ), + lowerArrayPosition( + lowerer, + call.arguments.nodes[2], + numLit(Infinity, loc), + "array copyWithin end", + ), receiverIr, loc, ); } if (name === "with") { - if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.with with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.with with ${call.arguments.nodes.length} arguments`, call); } const receiver = lowerer.lowerExpr(access.expression); - const index = lowerPositionArgument(lowerer, call.arguments[0]!, numLit(0, loc)); + const index = lowerPositionArgument(lowerer, call.arguments.nodes[0]!, numLit(0, loc)); if (!primitivePositionType(lowerer, index.type)) { - lowerer.noLowering(`.with index of '${lowerer.fmt(index.type)}' values`, call.arguments[0]!); + lowerer.noLowering( + `.with index of '${lowerer.fmt(index.type)}' values`, + call.arguments.nodes[0]!, + ); } - const value = lowerer.lowerExpr(call.arguments[1]!); + const value = lowerer.lowerExpr(call.arguments.nodes[1]!); const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr); const indexLocal = lowerer.declareHiddenLocal("%withIndex", index.type); const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type); @@ -534,7 +553,7 @@ export function lowerArrayMethodCall( lowerer, indexRef, numLit(0, loc), - call.arguments[0]!, + call.arguments.nodes[0]!, "array with index", ); const stmts: IrStmt[] = [ @@ -588,7 +607,7 @@ export function lowerArrayMethodCall( loc, }; } - const coercedValue = lowerer.coerceInto(call.arguments[1]!, valueRef, elem); + const coercedValue = lowerer.coerceInto(call.arguments.nodes[1]!, valueRef, elem); return { kind: "seqExpr", stmts, @@ -608,17 +627,17 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const start = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(0, loc), "array toSpliced start", ); const deleteCount = lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array toSpliced deleteCount", ); - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -709,7 +728,7 @@ export function lowerArrayMethodCall( | "reduce" | "reduceRight" ]; - if (call.arguments.length < arity[0]! || call.arguments.length > arity[1]!) { + if (call.arguments.nodes.length < arity[0]! || call.arguments.nodes.length > arity[1]!) { const hint = name === "map" || name === "filter" || @@ -721,7 +740,7 @@ export function lowerArrayMethodCall( ? "the thisArg parameter has no lowering — use an arrow function" : undefined; lowerer.noLowering( - `.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, + `.${name} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, call, hint, ); @@ -742,8 +761,8 @@ export function lowerArrayMethodCall( // order (count snapshotted first, so the self-spread forms duplicate // like JS) and return the new length. const spreadArg = - call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!) - ? call.arguments[0]! + call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!) + ? call.arguments.nodes[0]! : null; if ((name === "push" || name === "unshift") && spreadArg && ts.isSpreadElement(spreadArg)) { let src = lowerer.lowerExpr(spreadArg.expression); @@ -790,11 +809,11 @@ export function lowerArrayMethodCall( loc, }; } - if (call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.some(ts.isSpreadElement)) { // Finish argument evaluation before mutating the receiver. Each // spread snapshots its elements immediately, so a later argument // can mutate that source without changing already-collected values. - const items = lowerArraySpreadItems(lowerer, call.arguments, elem, receiverIr, loc); + const items = lowerArraySpreadItems(lowerer, call.arguments.nodes, elem, receiverIr, loc); return { kind: "arrIntrinsic", method: name === "push" ? "pushSpread" : "unshiftSpread", @@ -804,7 +823,7 @@ export function lowerArrayMethodCall( loc, }; } - const valueProbes = call.arguments.map((arg) => tryLowerExpression(lowerer, arg)); + const valueProbes = call.arguments.nodes.map((arg) => tryLowerExpression(lowerer, arg)); if ( valueProbes.some( (probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null, @@ -814,14 +833,14 @@ export function lowerArrayMethodCall( return lowerOptionalArrayPush( lowerer, receiver, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, loc, ); } const items = lowerArrayValueItems( lowerer, - call.arguments.map((arg) => lowerer.lowerExpr(arg)), + call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc, @@ -838,7 +857,7 @@ export function lowerArrayMethodCall( // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). - const args = call.arguments.map((a) => + const args = call.arguments.nodes.map((a) => name === "push" || name === "unshift" ? lowerer.lowerExprExpecting(a, elem) : lowerer.lowerExpr(a), @@ -886,7 +905,7 @@ export function lowerArrayMethodCall( const receiver = lowerer.lowerExpr(access.expression); const shape: (IrType | null)[] = []; const args: IrExpr[] = []; - for (const argNode of call.arguments) { + for (const argNode of call.arguments.nodes) { if (ts.isSpreadElement(argNode)) { lowerer.unsupported( "SC1090", @@ -928,26 +947,31 @@ export function lowerArrayMethodCall( if (name === "slice") { const receiver = lowerer.lowerExpr(access.expression); const args = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array slice start"), - lowerArrayPosition(lowerer, call.arguments[1], numLit(Infinity, loc), "array slice end"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array slice start"), + lowerArrayPosition( + lowerer, + call.arguments.nodes[1], + numLit(Infinity, loc), + "array slice end", + ), ]; return { kind: "arrIntrinsic", method: "slice", receiver, args, type: receiverIr, loc }; } if (name === "splice") { const receiver = lowerer.lowerExpr(access.expression); const args: IrExpr[] = [ - lowerArrayPosition(lowerer, call.arguments[0], numLit(0, loc), "array splice start"), + lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array splice start"), lowerArrayPosition( lowerer, - call.arguments[1], - numLit(call.arguments.length === 1 ? Infinity : 0, loc), + call.arguments.nodes[1], + numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc), "array splice deleteCount", ), ]; - if (call.arguments.length <= 2) { + if (call.arguments.nodes.length <= 2) { return { kind: "arrIntrinsic", method: "splice", receiver, args, type: receiverIr, loc }; } - const itemNodes = call.arguments.slice(2); + const itemNodes = call.arguments.nodes.slice(2); const hasSpread = itemNodes.some(ts.isSpreadElement); const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg)); const statefulItems = itemProbes.some( @@ -1007,8 +1031,8 @@ export function lowerArrayMethodCall( if (name === "join") { if (elem.kind === "dyn") { const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN); - const args = call.arguments.length - ? [lowerer.lowerExprExpecting(call.arguments[0]!, DYN)] + const args = call.arguments.nodes.length + ? [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)] : []; return lowerer.coerceToExpected( { @@ -1047,7 +1071,7 @@ export function lowerArrayMethodCall( ); } const receiver = lowerer.lowerExpr(access.expression); - const sep = lowerArrayJoinSeparator(lowerer, call.arguments[0], loc); + const sep = lowerArrayJoinSeparator(lowerer, call.arguments.nodes[0], loc); return { kind: "arrIntrinsic", method: "join", receiver, args: [sep], type: STRING, loc }; } if (name === "map" || name === "filter" || name === "forEach") { @@ -1247,8 +1271,8 @@ function lowerArrayFlatCall( receiverType: IrType & { kind: "array" }, ): IrExpr { const loc = locOf(call); - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.flat with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.flat with ${call.arguments.nodes.length} arguments`, call); } const elemCouldNest = (elem: IrType): boolean => elem.kind === "array" || @@ -1258,11 +1282,11 @@ function lowerArrayFlatCall( .get(elem.unionId) ?.arms.some((arm) => arm.kind === "array" || arm.kind === "dyn") ?? true)); - if (!elemCouldNest(receiverType.elem) && call.arguments[0]) { + if (!elemCouldNest(receiverType.elem) && call.arguments.nodes[0]) { const receiver = lowerer.lowerExpr(access.expression); const depth = lowerArrayPosition( lowerer, - call.arguments[0], + call.arguments.nodes[0], numLit(1, loc), "array flat depth", ); @@ -1286,7 +1310,9 @@ function lowerArrayFlatCall( loc, }; } - const depth = call.arguments[0] ? literalFlatDepth(lowerer.lowerExpr(call.arguments[0])) : 1; + const depth = call.arguments.nodes[0] + ? literalFlatDepth(lowerer.lowerExpr(call.arguments.nodes[0])) + : 1; if (depth === null) lowerer.noLowering(".flat with a nonconstant depth", call); let remaining = Number.isNaN(depth) || depth <= 0 ? 0 : Math.trunc(depth); let result: IrExpr = lowerer.lowerExpr(access.expression); @@ -1338,13 +1364,13 @@ function lowerArraySearchCall( ): IrExpr { const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(`.${method} with spread arguments`, call); - const needle: IrExpr = call.arguments[0] - ? lowerer.lowerExpr(call.arguments[0]) + const needle: IrExpr = call.arguments.nodes[0] + ? lowerer.lowerExpr(call.arguments.nodes[0]) : { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }; - const fromIndex: IrExpr = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const fromIndex: IrExpr = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : method === "lastIndexOf" ? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc } : numLit(0, loc); @@ -1361,7 +1387,7 @@ function lowerArraySearchValues( fromIndex: IrExpr, ): IrExpr { const loc = locOf(call); - const argNode = call.arguments[0] ?? call; + const argNode = call.arguments.nodes[0] ?? call; // A derived CLASS VALUE against a base-classval element widens (the same // pointer — identity search is exact); the coercion path owns the ABI gate. if ( @@ -1376,7 +1402,7 @@ function lowerArraySearchValues( // runtime implementation already has exact primitive/reference equality. if ( method !== "lastIndexOf" && - call.arguments.length === 1 && + call.arguments.nodes.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem) @@ -1398,7 +1424,7 @@ function lowerArraySearchValues( if (valueT.kind !== "union") { if (method === "lastIndexOf") lowerer.noLowering(".lastIndexOf on an array of checked values", call); - if (call.arguments.length > 1) + if (call.arguments.nodes.length > 1) lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call); if (!typeEquals(needle.type, elem)) lowerer.badType(argNode, lowerer.typeOf(argNode)); return { @@ -1456,7 +1482,7 @@ function lowerArraySearchValues( searchNeedle.type, directNeedle, fromIndex.type, - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, loc, ); return { @@ -1919,10 +1945,10 @@ export function lowerTupleReadMethodCall( elem, ); if (search) { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { return lowerer.noLowering(`tuple .${method} with spread or extra arguments`, call); } - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; const undefinedArg = arg ? lowerStaticallyUndefinedArgument(lowerer, arg) : null; let needle = arg ? (undefinedArg ?? lowerer.lowerExpr(arg)) : null; if (!needle || isUnitType(needle.type) || needle.type.kind === "void") { @@ -1934,10 +1960,10 @@ export function lowerTupleReadMethodCall( if (!wrapped) return lowerer.noLowering(`tuple .${method} missing search value`, call); needle = needle ? defaultAfterUndefined(needle, wrapped) : wrapped; } - const from = call.arguments[1] - ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc)) + const from = call.arguments.nodes[1] + ? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc)) : numLit(method === "lastIndexOf" ? Infinity : 0, loc); - const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.length}`; + const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.nodes.length}`; let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { @@ -1978,7 +2004,7 @@ export function lowerTupleReadMethodCall( lowerer, varRef("from.0", from.type, loc), numLit(0, loc), - call.arguments[1] ?? call, + call.arguments.nodes[1] ?? call, "tuple search position", ), loc, @@ -1991,13 +2017,13 @@ export function lowerTupleReadMethodCall( return { kind: "call", callee: helper, args: [receiver, needle, from], type: resultType, loc }; } if (method === "slice") { - if (call.arguments.length > 2 || call.arguments.some(ts.isSpreadElement)) { - lowerer.noLowering(`.slice with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) { + lowerer.noLowering(`.slice with ${call.arguments.nodes.length} arguments`, call); } - const args = call.arguments.map((arg) => lowerer.lowerExpr(arg)); + const args = call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)); args.forEach((arg, i) => { if (arg.type.kind !== "f64") - lowerer.badType(call.arguments[i]!, lowerer.typeOf(call.arguments[i]!)); + lowerer.badType(call.arguments.nodes[i]!, lowerer.typeOf(call.arguments.nodes[i]!)); }); const resultType = arrayOf(elem); const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; @@ -2039,14 +2065,14 @@ export function lowerTupleReadMethodCall( } return { kind: "call", callee: helper, args: [receiver, ...args], type: resultType, loc }; } - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments`, + `.${method} with ${call.arguments.nodes.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function", ); } - const callback = call.arguments[0]!; + const callback = call.arguments.nodes[0]!; // Check before the array HOF adapter can create a tuple-to-array copy. const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(callback)); if (signatures.some((signature) => signature.getParameters().length > 2)) { @@ -2210,10 +2236,10 @@ function lowerArrayAtCall( elem: IrType, ): IrExpr { const loc = locOf(call); - if (call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering(".at with spread arguments", call); const receiver = lowerer.lowerExpr(access.expression); - const indexNode = call.arguments[0]; + const indexNode = call.arguments.nodes[0]; const index = lowerPositionArgument(lowerer, indexNode, numLit(0, loc)); const resultT = lowerer.irTypeOf(call); if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 91cf0b263..c5a41cf32 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -20,6 +20,7 @@ import { arrayOf, bytesOf, funcOf, + isUnitType, typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -50,15 +51,15 @@ function lowerBytesSortCall( const loc = locOf(call); const method = access.name.text; const copyFirst = method === "toSorted"; - if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) { + if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering( - `.${method} with ${call.arguments.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, + `.${method} with ${call.arguments.nodes.length} arguments on ${BYTES_ELEMENT_NAME[bytesT.elem]}`, call, ); } const receiver = lowerer.lowerExprExpecting(access.expression, bytesT); - const undefinedArg = call.arguments[0] - ? lowerStaticallyUndefinedArgument(lowerer, call.arguments[0]) + const undefinedArg = call.arguments.nodes[0] + ? lowerStaticallyUndefinedArgument(lowerer, call.arguments.nodes[0]) : null; const helperFor = ( key: string, @@ -77,7 +78,7 @@ function lowerBytesSortCall( } return helper; }; - if (call.arguments.length === 0 || undefinedArg) { + if (call.arguments.nodes.length === 0 || undefinedArg) { const helper = helperFor("default", 0, false, null); if (!undefinedArg || isSafeToDiscard(undefinedArg)) { return { kind: "call", callee: helper, args: [receiver], type: bytesT, loc }; @@ -109,7 +110,7 @@ function lowerBytesSortCall( loc, }; } - const argNode = call.arguments[0]!; + const argNode = call.arguments.nodes[0]!; let fnArg = lowerer.lowerExpr(argNode); // As for Array sort: adapt a comparator over checked values to the // element ABI; arithmetic on inferred JS values still yields a number. @@ -189,7 +190,7 @@ export function lowerBytesNew( (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") && lowerer.isStdlibSymbol(symbol) ) { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) { lowerer.noLowering( "resizable ArrayBuffer construction", @@ -216,15 +217,15 @@ export function lowerBytesNew( const name = symbol.name; const type = bytesOf(elem); const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length === 0) return { kind: "bytesNew", source: null, type, loc }; if (args.length >= 2 && args.length <= 3 && !args.some(ts.isSpreadElement)) { return lowerArrayBufferView(lowerer, args, elem, loc); } if (args.length === 1 && !ts.isSpreadElement(args[0]!)) { const argNode = args[0]!; - if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) { - const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64)); + if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.nodes.some(ts.isSpreadElement)) { + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; return { kind: "bytesNew", source: seed, type, loc }; } @@ -277,9 +278,9 @@ export function lowerBytesStaticCall( ): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; if (lowerer.isStdlibGlobal(access.expression, "ArrayBuffer") && access.name.text === "isView") { - if (call.arguments.length !== 1 || call.arguments.some(ts.isSpreadElement)) + if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) lowerer.noLowering("ArrayBuffer.isView argument count", call); - const arg = call.arguments[0]!; + const arg = call.arguments.nodes[0]!; return { kind: "libCall", fn: "arrayBuffer.isView", @@ -293,15 +294,15 @@ export function lowerBytesStaticCall( const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; if ( - call.arguments.length < 1 || - call.arguments.length > 3 || - call.arguments.some(ts.isSpreadElement) + call.arguments.nodes.length < 1 || + call.arguments.nodes.length > 3 || + call.arguments.nodes.some(ts.isSpreadElement) ) { lowerer.noLowering("TypedArray.from with this argument count", call); } - const node = call.arguments[0]!; + const node = call.arguments.nodes[0]!; const source = lowerer.lowerExpr(node); - if (call.arguments.length > 1) { + if (call.arguments.nodes.length > 1) { if (!["string", "array", "bytes"].includes(source.type.kind)) lowerer.noLowering("TypedArray.from mapper over this source representation", call); const loc = locOf(call); @@ -356,9 +357,9 @@ export function lowerBytesStaticCall( callee: key, args: [ lowerer.coerceInto(node, source, DYN), - lowerer.lowerExprExpecting(call.arguments[1]!, DYN), - call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], DYN) + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN), + call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], DYN) : dynUndefinedExpr(loc), ], type: bytesOf(elem), @@ -414,7 +415,7 @@ function lowerArrayBufferView( } function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (!args.length || args.length > 3 || args.some(ts.isSpreadElement)) lowerer.noLowering("DataView constructor argument count", expr); return lowerArrayBufferView(lowerer, args, "dv", locOf(expr)); @@ -438,7 +439,7 @@ export function lowerBytesMethodCall( if (receiverIr?.kind !== "bytes") return null; if (!lowerer.isStdlibMember(access)) return null; const loc = locOf(call); - const nArgs = call.arguments.length; + const nArgs = call.arguments.nodes.length; // Encoded stream callbacks can receive strings even when JavaScript's // contextual inference still describes their parameter as a Buffer. if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) { @@ -451,8 +452,8 @@ export function lowerBytesMethodCall( fn: "dyn.toString", args: [ receiver, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), { kind: "strLit", value: access.getText(), type: STRING, loc }, ], @@ -492,7 +493,7 @@ export function lowerBytesMethodCall( }; } if (receiverIr.elem === "u8" && name === "with") { - if (nArgs !== 2 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.with with ${nArgs} arguments on Uint8Array`, call); } return { @@ -500,15 +501,15 @@ export function lowerBytesMethodCall( method: "with", receiver: lowerer.lowerExprExpecting(access.expression, receiverIr), args: [ - lowerer.lowerExprExpecting(call.arguments[0]!, F64), - lowerer.lowerExprExpecting(call.arguments[1]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64), + lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64), ], type: receiverIr, loc, }; } if (name === "join") { - if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 1 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.join with ${nArgs} arguments on typed arrays`, call); } const separatorDefault: IrExpr = { @@ -517,8 +518,8 @@ export function lowerBytesMethodCall( type: STRING, loc, }; - const separator = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], STRING, separatorDefault) + const separator = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], STRING, separatorDefault) : separatorDefault; return { kind: "bytesIntrinsic", @@ -536,7 +537,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, @@ -554,22 +555,22 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice"; 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 }; @@ -580,27 +581,25 @@ export function lowerBytesMethodCall( const symbol = lowerer.checker.getSymbolAtLocation(access.name); return ( symbol !== undefined && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration.parent) && - declaration.parent.name.text === "Buffer", - ) + symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration.parent) && + declaration.parent.name.text === "Buffer", + ) ); })() ) { // Typed arrays use element coercion and relative, clamped indices. // Buffer's same-named method keeps its pattern and offset validation. - if (nArgs > 3 || call.arguments.some(ts.isSpreadElement)) { + if (nArgs > 3 || call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`.fill with ${nArgs} arguments on typed arrays`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const zero: IrExpr = { kind: "numLit", value: 0, type: F64, loc }; - const v = call.arguments[0] - ? lowerOptionalArgument(lowerer, call.arguments[0], F64, zero) + const v = call.arguments.nodes[0] + ? lowerOptionalArgument(lowerer, call.arguments.nodes[0], F64, zero) : zero; - const idx = call.arguments.slice(1).map((a, i) => + const idx = call.arguments.nodes.slice(1).map((a, i) => lowerOptionalArgument(lowerer, a, F64, { kind: "numLit", value: i === 0 ? 0 : Infinity, @@ -622,14 +621,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, @@ -646,9 +645,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (receiverIr.elem !== "u8" || !declaredOnBuffer) { @@ -660,7 +659,7 @@ export function lowerBytesMethodCall( } if (nArgs > 3) lowerer.noLowering(`.toString with ${nArgs} arguments on Buffers`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const encNode = call.arguments[0]; + const encNode = call.arguments.nodes[0]; let method: IrBytesIntrinsicMethod = "toString"; let enc: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; if (encNode) { @@ -698,9 +697,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, @@ -724,9 +723,9 @@ export function lowerBytesMethodCall( const nameSym = lowerer.checker.getSymbolAtLocation(access.name); return ( nameSym !== undefined && - lowerer.checker - .declarationsOf(nameSym) - .some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer") + nameSym.declarations.some( + (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", + ) ); })(); if (declOnBuffer && receiverIr.elem === "u8") { @@ -741,7 +740,7 @@ export function lowerBytesMethodCall( lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offsetNode = call.arguments[required]; + const offsetNode = call.arguments.nodes[required]; const offset: IrExpr = offsetNode ? lowerer.lowerExprExpecting(offsetNode, F64) : { kind: "numLit", value: 0, type: F64, loc }; @@ -756,7 +755,7 @@ export function lowerBytesMethodCall( loc, }; } - const value = lowerer.lowerExprExpecting(call.arguments[0]!, BIGINT_T); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, BIGINT_T); return { kind: "libCall", fn: "bigint.bufferWrite", @@ -785,7 +784,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: numKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; if (nArgs === required) { args.push({ kind: "numLit", value: 0, type: F64, loc }); // Node's offset default } @@ -814,7 +813,7 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); const kind: IrExpr = { kind: "strLit", value: varKind, type: STRING, loc }; - const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))]; + const args = [kind, ...call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, F64))]; return { kind: "bytesIntrinsic", method: write ? "writeNumVar" : "readNumVar", @@ -833,10 +832,10 @@ export function lowerBytesMethodCall( if (dvBigGetter !== undefined && receiverIr.elem === "u8") { if (nArgs < 1 || nArgs > 2) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); const sign: IrExpr = { kind: "boolLit", value: dvBigGetter.sign, type: BOOL, loc }; - const le: IrExpr = call.arguments[1] - ? lowerer.lowerExprExpecting(call.arguments[1], BOOL) + const le: IrExpr = call.arguments.nodes[1] + ? lowerer.lowerExprExpecting(call.arguments.nodes[1], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -854,8 +853,8 @@ export function lowerBytesMethodCall( } const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); // ToIndex(undefined) is ToIndex(NaN): a missing offset reads offset 0. - const args = [lowerToNumberArgument(lowerer, call.arguments[0]!)]; - if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, BOOL)); + const args = [lowerToNumberArgument(lowerer, call.arguments.nodes[0]!)]; + if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BOOL)); return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc }; } // DataView setters — the getters' mirror: (byteOffset, value) plus the @@ -868,10 +867,10 @@ export function lowerBytesMethodCall( if ((name === "setBigUint64" || name === "setBigInt64") && receiverIr.elem === "u8") { if (nArgs < 2 || nArgs > 3) lowerer.noLowering(`.${name} with ${nArgs} arguments`, call); const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr); - const offset = lowerer.lowerExprExpecting(call.arguments[0]!, F64); - const value = lowerer.lowerExprExpecting(call.arguments[1]!, BIGINT_T); - const le: IrExpr = call.arguments[2] - ? lowerer.lowerExprExpecting(call.arguments[2], BOOL) + const offset = lowerer.lowerExprExpecting(call.arguments.nodes[0]!, F64); + const value = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, BIGINT_T); + const le: IrExpr = call.arguments.nodes[2] + ? lowerer.lowerExprExpecting(call.arguments.nodes[2], BOOL) : { kind: "boolLit", value: false, type: BOOL, loc }; return { kind: "libCall", @@ -890,10 +889,10 @@ export function lowerBytesMethodCall( // The offset (ToIndex) and the value (ToNumber) treat a missing value // as NaN. const args = [ - lowerToNumberArgument(lowerer, call.arguments[0]!), - lowerToNumberArgument(lowerer, call.arguments[1]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[0]!), + lowerToNumberArgument(lowerer, call.arguments.nodes[1]!), ]; - if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments[2]!, BOOL)); + if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[2]!, BOOL)); return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc }; } return null; @@ -984,7 +983,7 @@ function lowerBufferInstanceMethod( name: string, loc: SrcLoc, ): IrExpr | null { - const args = call.arguments; + const args = call.arguments.nodes; const nArgs = args.length; if (args.some(ts.isSpreadElement)) return null; const isStringArg = (i: number): boolean => @@ -1041,10 +1040,12 @@ function lowerBufferInstanceMethod( const fast = nArgs >= 1 && argIrKind(0)?.kind === "bytes" && - call.arguments.slice(1).every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); + call.arguments.nodes + .slice(1) + .every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64"); if (fast) { const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1121,7 +1122,7 @@ function lowerBufferInstanceMethod( if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.fill with ${nArgs} arguments`, call); // A trailing string-typed arg past the value is the encoding. let encNode: ts.Expression | undefined; - let idxNodes = call.arguments.slice(1); + let idxNodes = call.arguments.nodes.slice(1); if (idxNodes.length > 0 && isStringArg(nArgs - 1)) { encNode = idxNodes[idxNodes.length - 1]; idxNodes = idxNodes.slice(0, -1); @@ -1171,7 +1172,7 @@ function lowerBufferInstanceMethod( if (name === "copy") { if (nArgs < 1 || nArgs > 4) lowerer.noLowering(`.copy with ${nArgs} arguments`, call); const target = u8Arg(0); - const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); + const idx = call.arguments.nodes.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64)); const receiver = lowerer.lowerExprExpecting(access.expression, BYTES_U8); return { kind: "bytesIntrinsic", @@ -1192,7 +1193,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); @@ -1271,16 +1272,14 @@ function knownBufferProducer( const symbol = lowerer.checker.getSymbolAtLocation(node); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - ts.isVariableDeclarationList(decl.parent) && - (decl.parent.flags & ts.NodeFlags.Const) !== 0 && - decl.initializer !== undefined && - knownBufferProducer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 && + decl.initializer !== undefined && + knownBufferProducer(lowerer, decl.initializer, seen), + ); } if (!ts.isCallExpression(node)) return false; const callee = node.expression; @@ -1299,7 +1298,7 @@ function knownBufferProducer( if ( !ts.isPropertyAccessExpression(callee) || callee.name.text !== "digest" || - node.arguments.length !== 0 + node.arguments.nodes.length !== 0 ) return false; const type = lowerer.mapTypeOf(lowerer.typeOf(callee.expression)); @@ -1328,7 +1327,7 @@ function lowerBufferStaticValue( if (!lowerer.isStdlibGlobal(access.expression, "Buffer")) return null; const member = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (member === "from") { if ( args.length >= 1 && @@ -1371,11 +1370,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 }; } @@ -1424,7 +1426,14 @@ function lowerBufferStaticValue( } return { kind: "libCall", fn, args: [s, enc], type: BYTES_U8, loc }; } - if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") { + // A checker type with no mapping (pako's conditional `O extends + // { toText: true } ? string : Uint8Array` result) whose lowered + // value is a u8 view copies like the typed form. + if ( + args.length === 1 && + ((srcIr?.kind === "bytes" && srcIr.elem === "u8") || + (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8")) + ) { return { kind: "libCall", fn: "buffer.poolFromBytes", @@ -1442,6 +1451,34 @@ function lowerBufferStaticValue( loc, }; } + // A union of strings, u8 views and undefined/null (a JS-inferred + // npm result such as pako's deflate): the checked form dispatches + // on the runtime kind exactly as for an untyped JS value, Node's + // TypeError for the unit arms included. + if ( + args.length === 1 && + value.type.kind === "union" && + lowerer.dynConvertible(value.type) && + lowerer.unions + .get(value.type.unionId) + ?.arms.every( + (arm) => + arm.kind === "string" || + (arm.kind === "bytes" && arm.elem === "u8") || + isUnitType(arm), + ) === true + ) { + return { + kind: "libCall", + fn: "buffer.poolFromDyn", + args: [ + lowerer.coerceToExpected(value, DYN), + { kind: "strLit", value: "utf8", type: STRING, loc }, + ], + type: BYTES_U8, + loc, + }; + } } } lowerer.noLowering( @@ -1521,8 +1558,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); @@ -1654,6 +1691,12 @@ function lowerBufferStaticValue( }; } } + // A typed view of unknown provenance (a Uint8Array parameter, + // safe-buffer's Buffer.from result): Buffer is a brand of the view, + // which the runtime keeps, so the test reads it. + if (v.type.kind === "bytes") { + return { kind: "libCall", fn: "buffer.isBranded", args: [v], type: BOOL, loc }; + } lowerer.noLowering( `Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`, args[0]!, 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..2331ee25a 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]) { @@ -151,7 +153,7 @@ export function lowerForOfSearchParams( } : singleRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -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]) { @@ -353,7 +357,7 @@ export function lowerForOfArrayIter( } : keyRead(); if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, declT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value @@ -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]) { @@ -644,7 +650,7 @@ function lowerForOfMapOrSet( } : singleRead(); if (ts.isIdentifier(decl!.name)) { - const varTarget = forOfVarTarget(lowerer, decl!); + const varTarget = forOfVarTarget(lowerer, decl!, elemT!); if (varTarget) { // `for (var x of ...)`: one function-scoped binding, assigned // per pass — closures made in the loop share it, and the value diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index cca4bd51b..450e64142 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); @@ -804,6 +810,7 @@ export function lowerRecordOvfCaptureHelper( // Every source declared field must flow into the keyed-write slot — // in DECLARATION order (insertion order below; declaredOrder omits // internal '%'-fields, which never enter the copy). + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); @@ -1052,9 +1059,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,12 +1092,13 @@ 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); if (!from || from.tuple) return null; if (from.fields.some((f) => f.name.startsWith("%"))) return null; + lowerer.keyOrder.observed.add(from.id); const orderedFields = from.declaredOrder ? from.declaredOrder.flatMap((n) => { const f = from.fields.find((x) => x.name === n); @@ -1252,9 +1261,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 70e0d85e0..46074573d 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -39,6 +39,7 @@ import { iteratorSlotAdapter, lowerArrayIteratorValue, lowerBytesIteratorValue, + lowerCollectionIteratorValue, lowerStringIteratorValue, } from "../iterator-adapters.js"; import { lowerArrayCallback } from "./callback-arguments.js"; @@ -80,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; @@ -90,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 @@ -153,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 @@ -174,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 @@ -240,7 +275,7 @@ export function lowerHelperReturnCall(lowerer: Lowerer, call: ts.CallExpression) if (nonNull) callee = (callee as ts.NonNullExpression).expression; if (!nonNull && call.questionDotToken === undefined) return null; if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null; - if (callee.name.text !== "return" || call.arguments.length !== 0) return null; + if (callee.name.text !== "return" || call.arguments.nodes.length !== 0) return null; if (!isHelperObject(lowerer, callee.expression) || !lowerer.isStdlibMember(callee)) return null; const callT = lowerer.mapTypeOf(lowerer.typeOf(call)); const gen = lowerer.lowerExpr(callee.expression); @@ -263,7 +298,7 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { 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; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -281,8 +316,8 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean { * iterator, which already inherits from Iterator.prototype, so Node returns * it rather than a wrapper. A generator (including a helper object or an * array iterator) is itself; an array, string or typed array gives its - * values iterator; a Set its values and a Map its entries, adapted into a - * generator over the live collection. The result converts into the call's + * values iterator; a Set its values and a Map its entries, as a generator + * over the live collection. The result converts into the call's * IteratorObject type. Null for other arguments (objects with their own * next method, untyped values), which keep the Iterator fence. */ export function lowerIteratorFromCall( @@ -290,8 +325,8 @@ export function lowerIteratorFromCall( call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { - if (access.name.text !== "from" || call.arguments.length !== 1) return null; - const argNode = call.arguments[0]!; + if (access.name.text !== "from" || call.arguments.nodes.length !== 1) return null; + const argNode = call.arguments.nodes[0]!; if (ts.isSpreadElement(argNode)) return null; const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode)); @@ -317,16 +352,13 @@ export function lowerIteratorFromCall( iterator = lowerBytesIteratorValue(lowerer, value, "values", own(F64), loc); else if (t.kind === "set" || t.kind === "map") { const elem = t.kind === "set" ? t.elem : entryPairTypeOf(lowerer, t.key, t.value); - const native: IrExpr = { - kind: "dynInvoke", - recv: lowerer.coerceToExpected(value, DYN), - method: t.kind === "set" ? "values" : "entries", - calleeName: t.kind === "set" ? "set.values" : "map.entries", - args: [], - type: DYN, + iterator = lowerCollectionIteratorValue( + lowerer, + value, + t.kind === "set" ? "values" : "entries", + own(elem), loc, - }; - iterator = iteratorSlotAdapter(lowerer, native, own(elem)); + ); } if (iterator === null) lowerer.badType(argNode, lowerer.typeOf(argNode)); const result = lowerer.coerceToExpected(iterator, resultT); @@ -381,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; @@ -505,9 +541,13 @@ export function lowerForOfHelperChain( * argument directly. */ function iteratorFromArgument(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null { if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 1) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 1 + ) return null; - const arg = expr.arguments[0]!; + const arg = expr.arguments.nodes[0]!; if (ts.isSpreadElement(arg) || expr.expression.name.text !== "from") return null; if (!lowerer.isStdlibGlobal(expr.expression.expression, "Iterator")) return null; return lowerer.isStdlibMember(expr.expression) ? arg : null; @@ -525,7 +565,11 @@ function arrayIteratorCall( ? { receiver: fromArg, proj: "values" } : null; if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "values" && proj !== "entries") return null; @@ -578,7 +622,11 @@ function collectionIteratorCall( : null; } if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null; - if (expr.questionDotToken || expr.expression.questionDotToken || expr.arguments.length !== 0) + if ( + expr.questionDotToken || + expr.expression.questionDotToken || + expr.arguments.nodes.length !== 0 + ) return null; const proj = expr.expression.name.text; if (proj !== "keys" && proj !== "values" && proj !== "entries") return null; @@ -640,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) ); @@ -686,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 }; @@ -781,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 ( @@ -791,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 ( @@ -813,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; @@ -836,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, @@ -844,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 }; } @@ -937,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, @@ -961,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 }; @@ -1267,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( @@ -1639,9 +1800,15 @@ function foldStages( ...inner(value), ]; } else { + // A take before this stage sets `done` as it delivers its last + // value, which still flattens in full: the inner walk runs with the + // flag clear, and keeps it set afterwards. + const afterTake = slots.slice(0, si).some((slot) => slot.kind === "take"); emit = (value) => { const innerT = s.fnT!.ret; const innerId = frame.local("fm", innerT, false); + const heldId = afterTake ? frame.local("held", BOOL, false) : undefined; + const walk = walkInner(lowerer, frame, innerId, innerT, done(), inner, loc); return [ { kind: "varDecl", @@ -1650,7 +1817,26 @@ function foldStages( loc, }, ...(s.cntId !== undefined ? [frame.bump(s.cntId)] : []), - ...walkInner(lowerer, frame, innerId, innerT, done(), inner, loc), + ...(heldId === undefined + ? walk + : [ + { kind: "varDecl", localId: heldId, init: done(), loc } as IrStmt, + { kind: "assign", localId: doneId, value: boolLit(false, loc), loc } as IrStmt, + ...walk, + { + kind: "assign", + localId: doneId, + value: { + kind: "logical", + op: "||", + left: varRef(heldId, BOOL, loc), + right: done(), + type: BOOL, + loc, + }, + loc, + } as IrStmt, + ]), ]; }; } @@ -1978,7 +2164,7 @@ function buildIterChainFn( function buildHelperGenFn( lowerer: Lowerer, name: string, - srcT: GenType, + source: (ChainSource & { kind: "generator" }) | (ChainSource & { kind: "collection" }), srcElem: IrType, stage: StageIr, argT: IrType, @@ -1987,8 +2173,12 @@ function buildHelperGenFn( loc: SrcLoc, ): IrFunction { const frame = new LiftedFrame(loc); - const srcId = frame.param("src", srcT); + const srcId = frame.param("src", source.type); const argId = frame.param(stage.kind === "take" || stage.kind === "drop" ? "n" : "f", argT); + const drive = (pre: IrStmt[], each: (value: IrExpr) => IrStmt[], at: SrcLoc): IrStmt[] => + source.kind === "generator" + ? driveGenerator(lowerer, frame, srcId, source.type, srcElem, pre, each, at) + : driveCollectionNative(lowerer, frame, srcId, source, pre, each, at); const yieldOf = (value: IrExpr): IrStmt => ({ kind: "exprStmt", expr: { @@ -2014,12 +2204,7 @@ function buildHelperGenFn( const n = (): IrExpr => varRef(argId, F64, loc); if (stage.kind === "map") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const mId = frame.local("m", fnT!.ret, false); @@ -2034,12 +2219,7 @@ function buildHelperGenFn( ); } else if (stage.kind === "filter") { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const okId = frame.local("ok", BOOL, false); @@ -2070,28 +2250,12 @@ function buildHelperGenFn( loc, }, ]; - body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, - pre, - (value) => [yieldOf(value)], - loc, - ), - ); + body.push(...drive(pre, (value) => [yieldOf(value)], loc)); } else if (stage.kind === "drop") { const droppedId = frame.local("dropped", F64, true); body.push({ kind: "varDecl", localId: droppedId, init: numLit(0, loc), loc }); body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => [ { @@ -2115,12 +2279,7 @@ function buildHelperGenFn( ); } else { body.push( - ...driveGenerator( - lowerer, - frame, - srcId, - srcT, - srcElem, + ...drive( [], (value) => { const innerT = fnT!.ret; @@ -2142,7 +2301,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..39154a820 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,4 +1,12 @@ -import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js"; +import { matchAllRowsReadByElements } from "./match-row-uses.js"; +import { + boolLit, + dynUndefinedExpr, + nodeThrowExpr, + numLit, + strLit, + varRef, +} from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -315,6 +323,21 @@ export function lowerStringIndexCall( ) { index = lowerer.coerceInto(indexNode, index, DYN); } + return stringIndexHelperCall(lowerer, call, method, receiver, receiverNode, index, indexNode); +} + +/** The relative-index / code-point helper call over an already lowered + * index (any type positionNumber converts — dyn indices included). */ +function stringIndexHelperCall( + lowerer: Lowerer, + call: ts.CallExpression, + method: "at" | "codePointAt", + receiver: IrExpr, + receiverNode: ts.Node, + index: IrExpr, + indexNode: ts.Expression | undefined, +): IrExpr { + const loc = locOf(call); const valueType = method === "at" ? STRING : F64; const resultType = lowerer.withUndefinedArmOf(valueType); if (!resultType || resultType.kind !== "union") @@ -737,7 +760,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 +773,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 +842,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 +886,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 +905,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 +920,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 +936,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 +971,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 +1008,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 +1071,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") @@ -1057,7 +1083,8 @@ export function lowerStringMethodCall( const replaceMethod = STRING_REPLACE_METHODS.has(access.name.text) ? (access.name.text as "replace" | "replaceAll") : null; - if (!entry && !indexMethod && !replaceMethod) return null; + const extraMethod = EXTRA_STRING_METHODS.has(access.name.text); + if (!entry && !indexMethod && !replaceMethod && !extraMethod) return null; // A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse // value) arrives pre-extracted through `dynReceiver`; its checker type // is `any`, so the type/symbol gates don't apply — the dyn value's @@ -1085,6 +1112,12 @@ export function lowerStringMethodCall( return null; if (!lowerer.isStdlibMember(access)) return null; } + if (extraMethod && argumentNodes === call.arguments.nodes) { + const receiver = dynReceiver + ? dynReceiver() + : lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text); + return lowerExtraStringMethodCall(lowerer, call, access.name.text, receiver, access.expression); + } // The lib declares optional parameters beyond some lowered forms; fence // those arities instead of passing arguments the runtime doesn't take. if ( @@ -1368,17 +1401,407 @@ 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 }; } + +/* ── String.prototype / Number.prototype members beyond the core tables ── + * substr, the locale case pair, the Annex B HTML methods, toPrecision and + * toExponential(fractionDigits) on typed receivers, plus the kind dispatch + * for every String/Number method a checked-dynamic receiver can name + * whose runtime dispatch had no answer (`character.codePointAt(0)` in a + * JS package, `n.toPrecision(3)` on a JS-inferred number). Each path + * replaces a compile-time refusal, a runtime 'not supported yet' fence or + * Node's is-not-a-function answer for a real method, so programs that + * never spell these names compile exactly as before. */ + +/** String.prototype members (Annex B included) whose typed-receiver + * lowering lives here. */ +export const EXTRA_STRING_METHODS: ReadonlySet = new Set([ + "substr", + "toLocaleLowerCase", + "toLocaleUpperCase", + "anchor", + "big", + "blink", + "bold", + "fixed", + "fontcolor", + "fontsize", + "italics", + "link", + "small", + "strike", + "sub", + "sup", +]); + +/** String methods a dyn receiver dispatches by runtime kind here: a string + * runs the typed lowering, every other kind takes the runtime dispatch + * (an object's own member, Node's is-not-a-function otherwise). Only names + * no other dyn kind's prototype declares: the shared ones (at, concat, + * slice, ...) already dispatch at run time for arrays, and a kind test in + * front of them would change array code that works today. The Annex B + * HTML names (bold, italics, link, ...) stay out too: chalk-style builders + * call them on functions and objects, whose calls must stay as they are. */ +const DYN_STRING_KIND_METHODS: ReadonlySet = new Set([ + "substr", + "toLocaleLowerCase", + "toLocaleUpperCase", + "codePointAt", + "isWellFormed", + "toWellFormed", + "trimLeft", + "trimRight", +]); + +/** Number methods a dyn receiver dispatches by runtime kind here. */ +const DYN_NUMBER_KIND_METHODS: ReadonlySet = new Set(["toPrecision", "toExponential"]); + +/** Annex B CreateHTML: the tag and the attribute (null for none). */ +const HTML_METHODS: Readonly> = { + anchor: ["a", "name"], + big: ["big", null], + blink: ["blink", null], + bold: ["b", null], + fixed: ["tt", null], + fontcolor: ["font", "color"], + fontsize: ["font", "size"], + italics: ["i", null], + link: ["a", "href"], + small: ["small", null], + strike: ["strike", null], + sub: ["sub", null], + sup: ["sup", null], +}; + +export function isDynPrimitiveKindMethod(name: string): boolean { + return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name); +} + +/** Evaluate `value` once into a hidden local (effect-free reads stay). */ +function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr { + if ( + value.kind === "varRef" || + value.kind === "numLit" || + value.kind === "strLit" || + value.kind === "boolLit" || + value.kind === "unitLit" + ) + return value; + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return varRef(local.id, value.type, value.loc); +} + +function seq(stmts: IrStmt[], result: IrExpr): IrExpr { + return stmts.length === 0 + ? result + : { kind: "seqExpr", stmts, result, type: result.type, loc: result.loc }; +} + +/** ToString of a method argument: the dyn coercion (symbols throw), the + * typed conversions elsewhere. */ +function stringArg(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr { + if (value.type.kind === "dyn") + return { + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [value], + type: STRING, + loc: value.loc, + }; + return lowerer.ensureString(value, node); +} + +/** An optional numeric argument: its ToNumber value and whether it was + * supplied (undefined counts as absent, as the spec's checks do). */ +function optionalNumber( + lowerer: Lowerer, + value: IrExpr | undefined, + node: ts.Expression, + stmts: IrStmt[], + subject: string, + loc: SrcLoc, +): { value: IrExpr; has: IrExpr } { + if (!value || value.type.kind === "undefinedT" || value.type.kind === "void") + return { value: numLit(0, loc), has: boolLit(false, loc) }; + if ( + value.type.kind === "f64" || + value.type.kind === "string" || + value.type.kind === "bool" || + value.type.kind === "nullT" + ) + return { + value: positionNumber(lowerer, value, numLit(0, loc), node, subject), + has: boolLit(true, loc), + }; + const boxed = + value.type.kind === "dyn" + ? value + : hoist(lowerer, lowerer.coerceToExpected(value, DYN), stmts, "%numArg"); + const absent: IrExpr = { kind: "dynTest", test: "undefined", value: boxed, type: BOOL, loc }; + return { + value: positionNumber(lowerer, boxed, numLit(0, loc), node, subject), + has: { kind: "unary", op: "!", operand: absent, type: BOOL, loc }, + }; +} + +/** The typed result of a String.prototype member over a STRING receiver + * and already evaluated arguments, or null when the name is not one of + * this module's. */ +function buildStringMethod( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + s: IrExpr, + args: IrExpr[], + receiverNode: ts.Node, + stmts: IrStmt[], +): IrExpr | null { + const loc = locOf(call); + const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call; + if (name === "substr") { + const start = args[0] + ? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start") + : numLit(0, loc); + const length = optionalNumber(lowerer, args[1], node(1), stmts, "substr length", loc); + return { + kind: "libCall", + fn: "str.substr", + args: [s, start, length.value, length.has], + type: STRING, + loc, + }; + } + if (name === "toLocaleLowerCase" || name === "toLocaleUpperCase") { + const locales = args[0] ? lowerer.coerceToExpected(args[0], DYN) : dynUndefinedExpr(loc); + return { + kind: "libCall", + fn: "str.localeCase", + args: [s, locales, boolLit(name === "toLocaleUpperCase", loc)], + type: STRING, + loc, + }; + } + const html = Object.hasOwn(HTML_METHODS, name) ? HTML_METHODS[name] : undefined; + if (html) { + const [tag, attribute] = html; + const subject = hoist(lowerer, s, stmts, "%htmlSubject"); + const pieces: IrExpr[] = [strLit(`<${tag}`, loc)]; + if (attribute !== null) { + const text = args[0] ? stringArg(lowerer, args[0], node(0)) : strLit("undefined", loc); + pieces.push(strLit(` ${attribute}="`, loc)); + pieces.push({ + kind: "libCall", + fn: "str.replaceLiteral", + args: [text, strLit('"', loc), strLit(""", loc), boolLit(true, loc)], + type: STRING, + loc, + }); + pieces.push(strLit('"', loc)); + } + pieces.push(strLit(">", loc), subject, strLit(``, loc)); + return pieces.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc })); + } + if (name === "codePointAt") + return stringIndexHelperCall( + lowerer, + call, + name, + s, + receiverNode, + args[0] ?? numLit(0, loc), + call.arguments.nodes[0], + ); + if (name === "isWellFormed" || name === "toWellFormed") + return { + kind: "strIntrinsic", + method: name, + receiver: s, + args: [], + type: name === "isWellFormed" ? BOOL : STRING, + loc, + }; + if (name === "trimLeft" || name === "trimRight") + return { + kind: "strIntrinsic", + method: name === "trimLeft" ? "trimStart" : "trimEnd", + receiver: s, + args: [], + type: STRING, + loc, + }; + return null; +} + +/** The typed result of toPrecision/toExponential over an F64 receiver. */ +function buildNumberMethod( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + x: IrExpr, + args: IrExpr[], + stmts: IrStmt[], +): IrExpr | null { + const loc = locOf(call); + if (name !== "toPrecision" && name !== "toExponential") return null; + const digits = optionalNumber( + lowerer, + args[0], + call.arguments.nodes[0] ?? call, + stmts, + `${name} digits`, + loc, + ); + if (name === "toPrecision") + return { + kind: "libCall", + fn: "num.toPrecision", + args: [x, digits.value, digits.has], + type: STRING, + loc, + }; + const shortest: IrExpr = { + kind: "libCall", + fn: "num.toExponential", + args: [x], + type: STRING, + loc, + }; + if (digits.has.kind === "boolLit" && !digits.has.value) return shortest; + const fixed: IrExpr = { + kind: "libCall", + fn: "num.toExponentialDigits", + args: [x, digits.value], + type: STRING, + loc, + }; + if (digits.has.kind === "boolLit") return fixed; + return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc }; +} + +/** Evaluate the call's arguments, in order, into hidden locals. */ +function evaluatedArgs( + lowerer: Lowerer, + call: ts.CallExpression, + stmts: IrStmt[], + expected: IrType | undefined, +): IrExpr[] { + return call.arguments.nodes.map((arg) => + hoist( + lowerer, + expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg), + stmts, + "%methodArg", + ), + ); +} + +/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver, + * then arguments, then the method's own conversions — the spec order. */ +export function lowerExtraStringMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: IrExpr, + receiverNode: ts.Node, +): IrExpr | null { + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const stmts: IrStmt[] = []; + const s = hoist(lowerer, receiver, stmts, "%stringReceiver"); + const args = evaluatedArgs(lowerer, call, stmts, undefined); + const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts); + return result ? seq(stmts, result) : null; +} + +/** A typed F64 receiver calling toPrecision(p) or toExponential(f). */ +export function lowerExtraNumberMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + receiver: IrExpr, +): IrExpr | null { + if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length > 1) return null; + if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1)) + return null; + const stmts: IrStmt[] = []; + const x = hoist(lowerer, receiver, stmts, "%numberReceiver"); + const args = evaluatedArgs(lowerer, call, stmts, undefined); + const result = buildNumberMethod(lowerer, call, name, x, args, stmts); + return result ? seq(stmts, result) : null; +} + +/** `recv.m(args)` on a checked-dynamic receiver for a String/Number method + * the runtime dispatch cannot answer: the receiver and arguments evaluate + * once, a string (or number) receiver runs the typed lowering, and every + * other kind keeps the runtime dispatch's answer. Null for other names. */ +export function lowerDynPrimitiveMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + calleeName: string, + recv: IrExpr, +): IrExpr | null { + const stringMethod = DYN_STRING_KIND_METHODS.has(name); + const numberMethod = DYN_NUMBER_KIND_METHODS.has(name); + if (!stringMethod && !numberMethod) return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN); + stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc }); + const r = varRef(local.id, DYN, loc); + const args = evaluatedArgs(lowerer, call, stmts, DYN); + let result: IrExpr = { + kind: "dynInvoke", + recv: r, + method: name, + calleeName, + args, + type: DYN, + loc, + }; + const branch = ( + test: "string" | "number", + build: (value: IrExpr, branchStmts: IrStmt[]) => IrExpr | null, + ): void => { + const branchStmts: IrStmt[] = []; + const value: IrExpr = { + kind: "dynCheck", + value: r, + type: test === "string" ? STRING : F64, + loc, + }; + const built = build(value, branchStmts); + if (!built) return; + result = { + kind: "ternary", + cond: { kind: "dynTest", test, value: r, type: BOOL, loc }, + then: { kind: "dynFrom", value: seq(branchStmts, built), type: DYN, loc }, + else_: result, + type: DYN, + loc, + }; + }; + if (numberMethod) + branch("number", (x, branchStmts) => + buildNumberMethod(lowerer, call, name, x, args, branchStmts), + ); + if (stringMethod) + branch("string", (s, branchStmts) => + buildStringMethod(lowerer, call, name, s, args, call.expression, branchStmts), + ); + return seq(stmts, result); +} 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..e3ca2270d 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 => { @@ -283,7 +286,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(callee); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; if (!symbol || !declaration || !bindingNeverReassigned(lowerer, symbol, declaration)) return false; if (ts.isFunctionDeclaration(declaration)) return true; @@ -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) && @@ -343,7 +346,7 @@ function registryInitializer( } if (packageSource && ts.isPropertyAccessExpression(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node.name); - const declaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol && symbol.valueDeclaration; const owner = ts.isIdentifier(node.expression) ? lowerer.resolveValueSymbol(node.expression) : undefined; @@ -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) || @@ -391,7 +394,7 @@ function registryInitializer( ) return true; const symbol = lowerer.resolveValueSymbol(node); - const functionDeclaration = symbol && lowerer.checker.valueDeclarationOf(symbol); + const functionDeclaration = symbol && symbol.valueDeclaration; if ( functionDeclaration && ts.isFunctionDeclaration(functionDeclaration) && @@ -446,7 +449,7 @@ function registryOwnerIsData( symbol: ts.Symbol, statement: ts.Statement, ): boolean { - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || @@ -459,11 +462,11 @@ function registryOwnerIsData( ts.isCallExpression(initializer) && ts.isPropertyAccessExpression(initializer.expression) && lowerer.stdlibGlobalMember(initializer.expression, "Object") === "freeze" && - initializer.arguments.length === 1; - if (frozen) initializer = (initializer as ts.CallExpression).arguments[0]!; + initializer.arguments.nodes.length === 1; + if (frozen) initializer = (initializer as ts.CallExpression).arguments.nodes[0]!; if ( !ts.isObjectLiteralExpression(initializer) || - !initializer.properties.every( + !initializer.properties.nodes.every( (property) => ts.isShorthandPropertyAssignment(property) || (ts.isPropertyAssignment(property) && @@ -527,7 +530,7 @@ function classWriteIsUnobserved( const source = statement.getSourceFile(); const declaration = info.decl!; if ( - declaration.members.some( + declaration.members.nodes.some( (member) => ts.isClassStaticBlockDeclaration(member) || (ts.isPropertyDeclaration(member) && @@ -539,7 +542,7 @@ function classWriteIsUnobserved( ) ) return false; - for (const earlier of source.statements) { + for (const earlier of source.statements.nodes) { if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart()) continue; if ( @@ -551,7 +554,7 @@ function classWriteIsUnobserved( continue; if ( ts.isVariableStatement(earlier) && - earlier.declarationList.declarations.every( + earlier.declarationList.declarations.nodes.every( (binding) => !binding.initializer || registryInitializer(lowerer, binding.initializer, earlier), ) diff --git a/packages/compiler/src/frontend/lowering/devirtualize.ts b/packages/compiler/src/frontend/lowering/devirtualize.ts index b71080a3f..13f3f2dbd 100644 --- a/packages/compiler/src/frontend/lowering/devirtualize.ts +++ b/packages/compiler/src/frontend/lowering/devirtualize.ts @@ -249,14 +249,14 @@ function runtimeProvided(info: ClassInfo): boolean { function exportedNames(files: readonly ts.SourceFile[]): ReadonlySet { const names = new Set(); for (const sf of files) - for (const statement of sf.statements) { + for (const statement of sf.statements.nodes) { if ( ts.isExportDeclaration(statement) && statement.moduleSpecifier === undefined && statement.exportClause && ts.isNamedExports(statement.exportClause) ) { - for (const element of statement.exportClause.elements) + for (const element of statement.exportClause.elements.nodes) names.add((element.propertyName ?? element.name).text); } else if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression)) { names.add(statement.expression.text); diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index 2a9009aed..36fbf48b9 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -23,6 +23,7 @@ import { } from "../../diagnostics/diagnostic.js"; import { isCollationCovered } from "./collation-repertoire.js"; import type { Lowerer } from "./lowerer.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** How an identity-sensitive binding is used after a copy could replace it. */ interface IdentityUse { @@ -67,7 +68,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 +79,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 +88,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 +98,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 +109,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 +130,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 +165,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); @@ -177,13 +180,13 @@ export function scanDivergences( if (!candidates.some((fn) => identityUsesOf(fn).some((u) => u !== null))) continue; const signature = safely(() => checker.getResolvedSignature(call)); if (signature === undefined) continue; - const decl = safely(() => checker.signatureDeclaration(signature)); + const decl = safely(() => signature.declaration); 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; @@ -215,6 +218,8 @@ export function scanDivergences( } for (const { decl, source, type } of castDecls) { + // An assertion of the global object keeps that object: no copy. + if (keptIdentityAssertion(lowerer, decl.initializer!) !== undefined) continue; const name = (decl.name as ts.Identifier).text; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -230,11 +235,9 @@ export function scanDivergences( const left = assignment.left as ts.Identifier; const symbol = safely(() => checker.getSymbolAtLocation(left)); if (symbol === undefined) continue; - const decl = checker - .declarationsOf(symbol) - .find( - (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, - ); + const decl = symbol.declarations.find( + (d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d) && d.type !== undefined, + ); if (decl === undefined) continue; const scope = enclosingScope(decl); if (scope === undefined) continue; @@ -259,7 +262,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; @@ -285,7 +288,7 @@ export function scanDivergences( if (ts.isObjectLiteralExpression(inner)) lit = inner; else if (ts.isIdentifier(inner)) { const symbol = safely(() => checker.getSymbolAtLocation(inner)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -303,7 +306,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 +327,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 +351,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; @@ -371,12 +374,11 @@ export function scanDivergences( const name = calleeName(call); if (name === null || !fnsByName.has(name)) continue; const signature = safely(() => checker.getResolvedSignature(call)); - const decl = - signature === undefined ? undefined : safely(() => checker.signatureDeclaration(signature)); + const decl = signature === undefined ? undefined : safely(() => signature.declaration); 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); @@ -421,7 +423,7 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp if (stringLiteralText(arg) !== null) return [arg]; if (!ts.isIdentifier(arg)) return []; const symbol = safely(() => checker.getSymbolAtLocation(arg)); - const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol); + const decl = symbol === undefined ? undefined : symbol.valueDeclaration; const list = decl?.parent; const loop = list?.parent; if ( @@ -436,14 +438,14 @@ function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Exp let iterated: ts.Expression = loop.expression; if (ts.isIdentifier(iterated)) { const source = safely(() => checker.getSymbolAtLocation(iterated)); - const init = source === undefined ? undefined : checker.valueDeclarationOf(source); + const init = source === undefined ? undefined : source.valueDeclaration; if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined) return []; if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return []; 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") { @@ -741,7 +743,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - checker.declarationsOf(symbol).some((d) => !d.getSourceFile().isDeclarationFile) + symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile) ); } @@ -754,7 +756,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean { const symbol = type.getSymbol(); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; // Library-declared object types (Map, Date, Promise, ...) are not records. - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false; return true; } @@ -767,7 +769,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node const symbol = alias ?? type.getSymbol(); if (symbol === undefined) return null; if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null; - const decl = checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl; } @@ -779,7 +781,11 @@ function keyOrderFinding( written: readonly string[], assignedNames: ReadonlySet, ): { native: string; node: string; detail: "reordered" | "added-later" } | null { - const present = declared.filter((name) => written.includes(name)); + // A shape whose table the witness pass re-chose (key-order-witness.ts) + // enumerates that table, not the checker's declaration order. + const table = witnessTable(lowerer, contextual); + const native = table ?? declared; + const present = native.filter((name) => written.includes(name) && declared.includes(name)); const writtenKnown = written.filter((name) => declared.includes(name)); if (present.join("\0") !== writtenKnown.join("\0") && writtenKnown.length >= 2) { return { native: present.join(", "), node: writtenKnown.join(", "), detail: "reordered" }; @@ -795,12 +801,21 @@ function keyOrderFinding( const prop = checker.getPropertyOfType(contextual, name); if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) === 0) continue; const nodeOrder = [...writtenKnown, name]; - const nativeOrder = declared.filter((d) => nodeOrder.includes(d)); + const nativeOrder = native.filter((d) => nodeOrder.includes(d)); + if (table !== null && nativeOrder.join("\0") === nodeOrder.join("\0")) continue; return { native: nativeOrder.join(", "), node: nodeOrder.join(", "), detail: "added-later" }; } return null; } +/** The re-chosen enumeration table of the record shape a contextual type + * lowered to, or null when the witness pass kept today's table. */ +function witnessTable(lowerer: Lowerer, contextual: ts.Type): readonly string[] | null { + const mapped = safely(() => lowerer.mapTypeOf(contextual)); + if (mapped?.kind !== "record" || !lowerer.keyOrder.changed.has(mapped.shapeId)) return null; + return lowerer.shapes.get(mapped.shapeId)?.declaredOrder ?? null; +} + function classDeclarationOfTypeNode( lowerer: Lowerer, node: ts.TypeNode, @@ -808,9 +823,7 @@ function classDeclarationOfTypeNode( if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return null; const symbol = lowerer.checker.getSymbolAtLocation(node.typeName); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = lowerer.checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -821,9 +834,7 @@ function classDeclarationOfType( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; - const decl = checker - .declarationsOf(symbol) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -837,8 +848,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; @@ -846,9 +859,9 @@ function classExtends( (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - const decl = lowerer.checker - .declarationsOf(resolved) - .find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); + const decl = resolved.declarations.find( + (d) => ts.isClassDeclaration(d) || ts.isClassExpression(d), + ); cur = decl === undefined ? null : (decl as ts.ClassLikeDeclaration); } return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index 2d09efa1a..e3b9bf025 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -21,12 +21,12 @@ export function isNativeProxyInitializer( export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr); const handler = args[1]!; if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) { - for (const prop of handler.properties) { + for (const prop of handler.properties.nodes) { const callback = ts.isMethodDeclaration(prop) ? prop : ts.isPropertyAssignment(prop) && @@ -39,7 +39,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx // ProxyHandler types include symbols, which this boundary // explicitly refuses; they must not force a copying record // parameter or an unboxable string|symbol closure signature. - for (const param of callback.parameters) { + for (const param of callback.parameters.nodes) { if (ts.isIdentifier(param.name) && !param.type && !param.dotDotDotToken) lowerer.checkedCallbackParams.add(param); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 78d783a11..6238479e0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -24,6 +24,7 @@ import { PoisonError, neverTaintedJsType } from "../lowerer.js"; import type { Lowerer } from "../lowerer.js"; import { lowerIndexMergeHelper } from "../containers/indexed-objects.js"; import type { IndexMergeContributor } from "../containers/indexed-objects.js"; +import type { KeyOrderSegment } from "../key-order-witness.js"; import { PERF_ENTRY_MARKER, isGenericCallableMemberType, @@ -71,7 +72,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 +168,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 +247,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 +512,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 +563,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 +699,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 +712,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 +727,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 +755,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 +783,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 +803,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 +814,7 @@ export function lowerObjectLiteral( // message even when they also make the literal's type unmappable // (a getter or a `this`-returning method would otherwise surface as an // opaque type fence on the whole literal). - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { // Supported shapes: FULL spreads of known record shapes read as // identifiers or other side-effect-free reads, all BEFORE any @@ -935,7 +936,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 +955,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 +971,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 +980,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 +1037,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 +1063,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 +1081,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 +1136,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 +1162,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 +1199,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 +1227,14 @@ export function lowerObjectLiteral( expr.parent.initializer === expr && ts.isVariableStatement(expr.parent?.parent?.parent) && ts.isSourceFile(expr.parent?.parent?.parent?.parent); - const assignedProperties = expr.properties.filter( + const assignedProperties = expr.properties.nodes.filter( (prop): prop is ts.PropertyAssignment | ts.ShorthandPropertyAssignment => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop), ); if ( topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) && - assignedProperties.length === expr.properties.length + assignedProperties.length === expr.properties.nodes.length ) { const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name))); if (shape.fields.some((field) => !provided.has(field.name))) { @@ -1261,11 +1264,11 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && !shapeHasAccessorSlots(shape) && - expr.properties.length >= 2 && - ts.isSpreadAssignment(expr.properties[0]!) && - !conditionalSpreadOf(expr.properties[0]!.expression) && - !andSpreadOf(lowerer, expr.properties[0]!.expression) && - expr.properties + expr.properties.nodes.length >= 2 && + ts.isSpreadAssignment(expr.properties.nodes[0]!) && + !conditionalSpreadOf(expr.properties.nodes[0]!.expression) && + !andSpreadOf(lowerer, expr.properties.nodes[0]!.expression) && + expr.properties.nodes .slice(1) .every( (p) => @@ -1277,8 +1280,8 @@ export function lowerObjectLiteral( (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)), ) ) { - const spread = expr.properties[0]! as ts.SpreadAssignment; - const props = expr.properties.slice(1) as ( + const spread = expr.properties.nodes[0]! as ts.SpreadAssignment; + const props = expr.properties.nodes.slice(1) as ( | ts.PropertyAssignment | ts.ShorthandPropertyAssignment )[]; @@ -1334,12 +1337,12 @@ export function lowerObjectLiteral( shape.indexValue && shape.fields.length === 0 && !shape.tuple && - (expr.properties.some((p) => ts.isSpreadAssignment(p)) || - expr.properties.some(isRuntimeComputedKey)) + (expr.properties.nodes.some((p) => ts.isSpreadAssignment(p)) || + expr.properties.nodes.some(isRuntimeComputedKey)) ) { const contributors: IndexMergeContributor[] = []; let mergeable = true; - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (ts.isSpreadAssignment(prop)) { const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression); if (cs === "unsupported" || (cs && cs.props.length !== 1)) { @@ -1463,7 +1466,7 @@ export function lowerObjectLiteral( } // Unfoldable computed keys outside the index-signature merge path: // no record shape can hold a runtime-decided name. - for (const prop of expr.properties) { + for (const prop of expr.properties.nodes) { if (isRuntimeComputedKey(prop)) { lowerer.unsupported( "SC1090", @@ -1481,14 +1484,14 @@ export function lowerObjectLiteral( !shape.indexValue && !shape.tuple && shape.fields.length > 0 && - expr.properties.length > 0 && - expr.properties.every( + expr.properties.nodes.length > 0 && + expr.properties.nodes.every( (p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression) && !andSpreadOf(lowerer, p.expression), ) && - expr.properties.some((p) => { + expr.properties.nodes.some((p) => { const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression)); return ( t?.kind === "dyn" || (t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue) @@ -1515,7 +1518,7 @@ export function lowerObjectLiteral( const fields: { name: string; value: IrExpr; overflow?: true }[] = []; const spreadStmts: IrStmt[] = []; - const hasSpread = expr.properties.some(ts.isSpreadAssignment); + const hasSpread = expr.properties.nodes.some(ts.isSpreadAssignment); const captured = new Set(); const capture = (value: IrExpr): IrExpr => { const local = lowerer.declareHiddenLocal("%spreadValue", value.type); @@ -1594,7 +1597,9 @@ 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) { + // The order this literal creates its keys in (key-order-witness.ts). + const orderSegments: KeyOrderSegment[] = []; + 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. @@ -1649,6 +1654,7 @@ export function lowerObjectLiteral( v = lowerer.coerceInto(valueNode, v, fieldType); if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode)); conditionalNames.add(name); + orderSegments.push({ kind: "key", name, definite: false }); fields.push({ name, value: { @@ -1672,7 +1678,7 @@ export function lowerObjectLiteral( let srcNode: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression; const srcLowered = capture( - prop === expr.properties[0] && leadingSpreadLowered !== null + prop === expr.properties.nodes[0] && leadingSpreadLowered !== null ? leadingSpreadLowered : lowerer.lowerExpr(srcNode), ); @@ -1732,7 +1738,7 @@ export function lowerObjectLiteral( ); } const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -1756,6 +1762,7 @@ export function lowerObjectLiteral( } } const srcRef = (): IrExpr => srcLowered ?? lowerer.lowerExpr(srcNode); + orderSegments.push({ kind: "spread", shapeId: recArm.shapeId, maybeAbsent: true }); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; const targetType = fieldTypes.get(f.name); @@ -1968,7 +1975,7 @@ export function lowerObjectLiteral( // exactly like Node does. Conditional spreads don't count (their // empty arm defines nothing) — their own collision fences hold. const laterNames = new Set(); - for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) { + for (const later of expr.properties.nodes.slice(expr.properties.nodes.indexOf(prop) + 1)) { if (ts.isSpreadAssignment(later)) { if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression)) continue; @@ -1991,6 +1998,12 @@ export function lowerObjectLiteral( laterNames.add(propNameText(lowerer, later.name)); } } + // A checked-dynamic source's keys are a run-time fact. + orderSegments.push( + srcLowered.type.kind === "dyn" + ? { kind: "opaque" } + : { kind: "spread", shapeId: srcType.shapeId, maybeAbsent: false }, + ); for (const f of srcShape.fields) { if (laterNames.has(f.name)) continue; let targetType = fieldTypes.get(f.name); @@ -2214,7 +2227,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) @@ -2305,6 +2318,7 @@ export function lowerObjectLiteral( const at = fields.findIndex((x) => x.name === name); if (at >= 0) fields.splice(at, 1); fields.push({ name, value }); + orderSegments.push({ kind: "key", name, definite: true }); } // Optional fields may be omitted: the field holds its absent state. // tsc rejects omission of required fields before lowering. @@ -2333,7 +2347,10 @@ export function lowerObjectLiteral( fields.push({ name: f.name, value: absent }); } } - const result: IrExpr = { kind: "recordLit", fields, type, loc }; + const result: IrExpr = lowerer.keyOrder.literal( + { kind: "recordLit", fields, type, loc }, + orderSegments, + ); return spreadStmts.length === 0 ? result : { kind: "seqExpr", stmts: spreadStmts, result, type, loc }; @@ -2347,7 +2364,7 @@ export function lowerObjectLiteral( * fields needs a different result layout and stays on the ordinary path. */ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null { if (isJsSourceFile(expr.getSourceFile())) return null; - const spread = expr.properties[0]; + const spread = expr.properties.nodes[0]; if (!spread || !ts.isSpreadAssignment(spread)) return null; let sourceNode = spread.expression; while (ts.isParenthesizedExpression(sourceNode) || ts.isSatisfiesExpression(sourceNode)) @@ -2361,7 +2378,7 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "record")) return null; const props: (ts.PropertyAssignment | ts.ShorthandPropertyAssignment)[] = []; const names: string[] = []; - for (const prop of expr.properties.slice(1)) { + for (const prop of expr.properties.nodes.slice(1)) { if ( (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) || (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) @@ -2557,9 +2574,9 @@ export function andSpreadOf( ) ) return null; - if (right.properties.length === 0) return "unsupported"; + if (right.properties.nodes.length === 0) return "unsupported"; const props: CondSpreadProp[] = []; - for (const p of right.properties) { + for (const p of right.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2595,12 +2612,14 @@ export function conditionalSpreadOf( if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) { return "unsupported"; } - const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0; - const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0; + const trueCarries = + whenFalse.properties.nodes.length === 0 && whenTrue.properties.nodes.length > 0; + const falseCarries = + whenTrue.properties.nodes.length === 0 && whenFalse.properties.nodes.length > 0; if (!trueCarries && !falseCarries) return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar const carrier = trueCarries ? whenTrue : whenFalse; const props: CondSpreadProp[] = []; - for (const p of carrier.properties) { + for (const p of carrier.properties.nodes) { if ( (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && p.name && @@ -2643,7 +2662,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") { @@ -3043,7 +3062,7 @@ export function lowerShorthandValue( return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName); } const sig = lowerer.fnSigsBySymbol.get(resolved); - const decl = lowerer.checker.declarationsOf(resolved)[0]; + const decl = resolved.declarations[0]; if ( sig && decl && diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 55feafc62..930e1a7cb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -253,7 +253,7 @@ export function lowerOptionalChain( } } const spreads: { arg: number; what: string }[] = []; - const args = expr.arguments.map((arg, index) => { + const args = expr.arguments.nodes.map((arg, index) => { if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); spreads.push({ arg: index, what: arg.expression.getText() }); return lowerer.lowerExprExpecting(arg.expression, DYN); @@ -429,7 +429,7 @@ export function lowerOptionalChain( if (dotNode === expr && ts.isCallExpression(expr)) { if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode)); const params = narrowed.params; - const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); + const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i])); const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc }; return lowerer.finishOptionalChain(expr, id, receiver, body, loc); } diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index a3cf5c0ce..f413f4680 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -148,7 +148,7 @@ export function declaredReturnType( let mappedReturn = lowerer.mapTypeOf(retTsType); if ( isJsSourceFile(decl.getSourceFile()) && - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !decl.asteriskToken && mappedReturn?.kind !== "promise" ) { @@ -240,8 +240,8 @@ export function declaredReturnType( !hasExplicitJsDocReturn(decl) && mappedReturn !== null && promiseCarriesDyn(lowerer, mappedReturn) && - !decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && - decl.parameters.some((param) => { + !decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.parameters.nodes.some((param) => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(param.name)); return mapped === null || mapped.kind === "dyn"; }) @@ -278,7 +278,7 @@ export function declaredReturnType( // Native-only members (such as Int32Array layout offsets) cannot cross // that boundary; their factories keep the native record ABI. const asyncReturn = - decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && + decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && mappedReturn?.kind === "promise"; const recordReturn = asyncReturn && mappedReturn?.kind === "promise" ? mappedReturn.inner : mappedReturn; @@ -303,7 +303,7 @@ export function declaredReturnType( recordReturn !== undefined && inferredRecordReturn && lowerer.dynConvertible(recordReturn) && - decl.parameters.every((p) => { + decl.parameters.nodes.every((p) => { const mt = lowerer.mapTypeOf(lowerer.typeOf(p)); return mt === null || mt.kind === "dyn"; }) diff --git a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts index 10505e063..ca6a5ecdc 100644 --- a/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/generator-dynamic-dispatch.ts @@ -7,6 +7,7 @@ import { funcOf, typeEquals, typeKey, + VOID, type IrExpr, type IrFunction, type IrStmt, @@ -18,6 +19,22 @@ import type { Lowerer } from "./lowerer.js"; type GeneratorType = Extract; type Invoke = Extract; +type KeyGet = Extract; + +/** The iterator helper methods the runtime answers on checked iterators. */ +const HELPER_METHODS = new Set([ + "map", + "filter", + "take", + "drop", + "flatMap", + "reduce", + "toArray", + "forEach", + "some", + "every", + "find", +]); /** An untyped consumer resumes the original native fiber. The capsule's * exact type key selects the channel conversions and result layout. */ @@ -29,6 +46,54 @@ export class GeneratorDynamicDispatch { >(); private readonly generated = new Set(); private readonly properties = new Map }>(); + /** Whether %main registers the next/return getters with the runtime. */ + private membersRegistered = false; + + /** The getter for a generator capsule's `method` (next, return, throw or + * an iterator helper): a checked function over the capsule's type keys + * that answers the method as a function value. `read` is the dyn read + * whose fallback it keeps for other receivers. */ + private ensureProperty(lowerer: Lowerer, method: string, read: KeyGet): IrFunction { + const existing = this.properties.get(method); + if (existing) return existing.fn; + const loc = read.loc; + const fn: IrFunction = { + name: `%dyn.generator.get.${method}`, + params: [{ localId: "value", name: "value", type: DYN }], + returnType: DYN, + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + body: [{ kind: "return", value: { ...read, value: varRef("value", DYN, loc) }, loc }], + loc, + }; + this.properties.set(method, { fn, types: new Set() }); + this.generated.add(fn); + lowerer.liftedFns.push(fn); + // The runtime's own helper functions answer the helper methods. + if (HELPER_METHODS.has(method)) return fn; + lowerer.liftedFns.push({ + name: `%dyn.generator.method.${method}`, + params: [{ localId: "arg", name: "arg", type: DYN }], + returnType: DYN, + locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }], + body: [ + { + kind: "return", + value: { + kind: "dynInvoke", + method, + calleeName: `Generator.${method}`, + recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, + args: [varRef("arg", DYN, loc)], + type: DYN, + loc, + }, + loc, + }, + ], + loc, + }); + return fn; + } process( lowerer: Lowerer, @@ -37,6 +102,7 @@ export class GeneratorDynamicDispatch { ): boolean { const seen = new Set(); const rewrite = new Set(); + let runtimeSteps = false; const discover = (type: IrType): void => { const key = typeKey(type); if (seen.has(key)) return; @@ -56,10 +122,22 @@ export class GeneratorDynamicDispatch { if (expr.kind === "dynFrom") discover(expr.value.type); if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method)) rewrite.add(fn); + // The runtime steps the values iterator helpers and destructuring + // receive. + if ( + (expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) || + (expr.kind === "dynKeyGet" && + expr.key.kind === "strLit" && + HELPER_METHODS.has(expr.key.value)) || + expr.kind === "dynIterN" || + (expr.kind === "libCall" && expr.fn === "dyn.destructureRest") + ) + runtimeSteps = true; if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - ["next", "return", "throw"].includes(expr.key.value) + (["next", "return", "throw"].includes(expr.key.value) || + HELPER_METHODS.has(expr.key.value)) ) rewrite.add(fn); return true; @@ -74,56 +152,20 @@ export class GeneratorDynamicDispatch { fn.body = transformStmtList(fn.body, { stmt: (stmt) => stmt, expr: (expr) => { + // A helper method read reaches the runtime's helpers through the + // same getters once generators are boxed. if ( expr.kind === "dynKeyGet" && expr.key.kind === "strLit" && - ["next", "return", "throw"].includes(expr.key.value) + (["next", "return", "throw"].includes(expr.key.value) || + (HELPER_METHODS.has(expr.key.value) && this.generators.size > 0)) ) { const method = expr.key.value; - let property = this.properties.get(method); - if (!property) { - const loc = expr.loc; - const fn: IrFunction = { - name: `%dyn.generator.get.${method}`, - params: [{ localId: "value", name: "value", type: DYN }], - returnType: DYN, - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - body: [ - { kind: "return", value: { ...expr, value: varRef("value", DYN, loc) }, loc }, - ], - loc, - }; - property = { fn, types: new Set() }; - this.properties.set(method, property); - this.generated.add(fn); - lowerer.liftedFns.push(fn); - lowerer.liftedFns.push({ - name: `%dyn.generator.method.${method}`, - params: [{ localId: "arg", name: "arg", type: DYN }], - returnType: DYN, - locals: [{ id: "arg", name: "arg", type: DYN, mutable: false }], - body: [ - { - kind: "return", - value: { - kind: "dynInvoke", - method, - calleeName: `Generator.${method}`, - recv: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc }, - args: [varRef("arg", DYN, loc)], - type: DYN, - loc, - }, - loc, - }, - ], - loc, - }); - changed = true; - } + if (!this.properties.has(method)) changed = true; + const getter = this.ensureProperty(lowerer, method, expr); return { kind: "call", - callee: property.fn.name, + callee: getter.name, args: [expr.value], type: DYN, loc: expr.loc, @@ -179,6 +221,50 @@ export class GeneratorDynamicDispatch { }, }); } + // Iterator helpers called on untyped values may reach a compiled + // generator's capsule; the runtime drives it through these getters, + // which %main registers first. + const firstFn = functions[0]; + if ( + runtimeSteps && + !this.membersRegistered && + firstFn !== undefined && + [...this.generators.values()].some((t) => t.async !== true) + ) { + const loc = firstFn.loc; + const read = (method: string): KeyGet => ({ + kind: "dynKeyGet", + value: varRef("value", DYN, loc), + key: { kind: "strLit", value: method, type: STRING, loc }, + type: DYN, + loc, + }); + const getterValue = (method: string): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName: this.ensureProperty(lowerer, method, read(method)).name, + captures: [], + type: funcOf([DYN], DYN), + loc, + }, + type: DYN, + loc, + }); + lowerer.mainPrelude.push({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.setGeneratorMembers", + args: [getterValue("next"), getterValue("return")], + type: VOID, + loc, + }, + loc, + }); + this.membersRegistered = true; + changed = true; + } for (const [method, property] of this.properties) for (const [key] of this.generators) { if (property.types.has(key)) continue; @@ -196,18 +282,26 @@ export class GeneratorDynamicDispatch { then: [ { kind: "return", - value: { - kind: "dynFrom", - value: { - kind: "closure", - fnName: `%dyn.generator.method.${method}`, - captures: [], - type: funcOf([DYN], DYN), - loc, - }, - type: DYN, - loc, - }, + value: HELPER_METHODS.has(method) + ? { + kind: "libCall", + fn: "dyn.iteratorHelperMethod", + args: [{ kind: "strLit", value: method, type: STRING, loc }], + type: DYN, + loc, + } + : { + kind: "dynFrom", + value: { + kind: "closure", + fnName: `%dyn.generator.method.${method}`, + captures: [], + type: funcOf([DYN], DYN), + loc, + }, + type: DYN, + loc, + }, loc, }, ], @@ -228,10 +322,14 @@ export class GeneratorDynamicDispatch { const supplied = dispatch.source.args.length > 0; const incoming = supplied ? varRef("p.1", DYN, loc) : dynUndefinedExpr(loc); const channel = mode === "next" ? type.nextT : type.retT; + // return() with no value completes with undefined, as the typed + // .return() does, whatever the return channel's type. const arg = mode === "throw" ? incoming - : channel.kind === "undefinedT" || channel.kind === "void" + : channel.kind === "undefinedT" || + channel.kind === "void" || + (mode === "return" && !supplied) ? null : lowerer.coerceToExpected(incoming, channel); const convertible = canConvertToDyn( @@ -263,25 +361,36 @@ export class GeneratorDynamicDispatch { else_: null, loc, }); + const resume = (sent: IrExpr | null): IrStmt => ({ + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "genResume", + mode, + gen: { kind: "dynCheck", value: receiver, type, loc }, + arg: sent, + type: resultType, + loc, + }, + type: DYN, + loc, + }, + loc, + }); + const valid = + convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel)); + if (valid && mode === "return" && arg !== null) + body.push({ + kind: "if", + cond: { kind: "dynTest", test: "undefined", value: incoming, type: BOOL, loc }, + then: [resume(null)], + else_: null, + loc, + }); body.push( - convertible && (arg === null || mode === "throw" || typeEquals(arg.type, channel)) - ? { - kind: "return", - value: { - kind: "dynFrom", - value: { - kind: "genResume", - mode, - gen: { kind: "dynCheck", value: receiver, type, loc }, - arg, - type: resultType, - loc, - }, - type: DYN, - loc, - }, - loc, - } + valid + ? resume(arg) : { kind: "runtimeFence", code: "SC1071", diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 67dfb2408..f588faf6d 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -187,9 +187,9 @@ function requireStableGenericBinding( name: string, ): void { const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(symbol) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = symbol.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (!isConst && (redeclared || !bindingNeverReassigned(lowerer, symbol, decl))) { lowerer.unsupported( "SC1090", @@ -207,11 +207,11 @@ function requireStableGenericBinding( export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDeclaration): void { const typeParams = declaredTypeParameterSymbols( lowerer, - decl.typeParameters!, + decl.typeParameters!.nodes, decl, "this function form", ); - checkGenericParameterNames(lowerer, decl.parameters, true); + checkGenericParameterNames(lowerer, decl.parameters.nodes, true); const nameText = decl.name?.text ?? "%default"; // nameless = the default export (checked by collectSignatureInner) const symbol = declSymbolOf(lowerer, decl); if (!symbol) lowerer.unsupported("SC1090", decl, "this function form"); @@ -245,7 +245,7 @@ export function lowerGenericCall( const instance = info.implicitParams ? implicitCallInstance(lowerer, expr, info) : genericCallInstance(lowerer, expr, info); - const args = lowerer.completeArgs(expr.arguments, instance.params, loc, expr); + const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr); return { kind: "call", callee: instance.name, args, type: instance.returnType, loc }; } @@ -266,7 +266,7 @@ export function genericCallInstance( // honest story (monomorphize per implementation signature with the // reconcile bridge, like the non-generic path). { - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; if ( rdecl && (ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) && @@ -279,7 +279,7 @@ export function genericCallInstance( ); } } - if (!rsig || rsig.getParameters().length !== info.decl.parameters.length) { + if (!rsig || rsig.getParameters().length !== info.decl.parameters.nodes.length) { lowerer.unsupported("SC1090", expr, "this call form"); } // Per-param shapes from the RESOLVED signature (types substituted) plus @@ -288,9 +288,9 @@ export function genericCallInstance( // paramShape rules, applied to post-substitution types. const params = paramShapes( lowerer, - info.decl.parameters, + info.decl.parameters.nodes, rsig, - (_param, i) => expr.arguments[i] ?? expr, + (_param, i) => expr.arguments.nodes[i] ?? expr, ); const retTs = lowerer.checker.getReturnTypeOfSignature(rsig); const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs); @@ -438,7 +438,7 @@ export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrTyp /** Whether `node` is declared inside a function with type parameters. */ function insideGenericFunction(node: ts.Node): boolean { for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true; + if (ts.isFunctionLike(parent) && (parent.typeParameters?.nodes.length ?? 0) > 0) return true; return false; } @@ -451,7 +451,7 @@ function localClassOfResult( ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) if (parent === info.decl) return decl; @@ -556,7 +556,7 @@ function internGenericInstance( function keyofConstrainedTypeParams(info: GenericFnInfo): Set { if (info.keyofTps) return info.keyofTps; const out = new Set(); - info.decl.typeParameters?.forEach((tpDecl, i) => { + info.decl.typeParameters?.nodes.forEach((tpDecl, i) => { const sym = info.typeParams[i]; if (!sym || tpDecl.constraint === undefined) return; if ( @@ -586,7 +586,7 @@ export function inferTypeParamBindings( tsBindings?: Map, ): Map { const bindings = new Map(); - expr.typeArguments?.forEach((ta, i) => { + expr.typeArguments?.nodes.forEach((ta, i) => { const tp = info.typeParams[i]; if (!tp) return; const taT = lowerer.checker.getTypeFromTypeNode(ta); @@ -597,7 +597,13 @@ export function inferTypeParamBindings( } }); unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings, tsBindings); + bindDefaultTypeParams( + lowerer, + info.typeParams, + info.decl.typeParameters?.nodes, + bindings, + tsBindings, + ); return bindings; } @@ -847,7 +853,7 @@ export function lowerGenericInstance( // body returns the resolved promise's INNER type, calls enter through // the instance's own spawn wrapper (the emitter routes by fn.async), // and awaits park this instance's fibers. - const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const nameBlame: ts.Node = (ts.isArrowFunction(decl) ? undefined : decl.name) ?? decl; const isGenerator = decl.asteriskToken !== undefined; if (isAsync && !isGenerator && inst.returnType.kind !== "promise") { @@ -909,12 +915,12 @@ export function lowerGenericInstance( } // Default-param initializers lower per instance, with the bindings // threaded — a default mentioning T resolves like any body expression. - const declared = lowerer.declareParams(decl.parameters, inst.params); + const declared = lowerer.declareParams(decl.parameters.nodes, inst.params); params.push(...declared.params); const body = [...receiverInit, ...declared.prologue]; const bodyBlock = blockBodyOf(decl); if (bodyBlock) { - body.push(...lowerer.lowerStmts(bodyBlock.statements)); + body.push(...lowerer.lowerStmts(bodyBlock.statements.nodes)); if (fnCtx.inferReturn) { bodyReturn = resolveInferredReturn(lowerer, inst, fnCtx.inferReturn, body, decl); inst.returnType = bodyReturn; @@ -1053,7 +1059,7 @@ export function lowerGenericFnValue( unifySignatureBindings(lowerer, info, target, bindings); const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info); if (hofBinding) bindings.set(hofBinding.typeParam, hofBinding.type); - bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters, bindings); + bindDefaultTypeParams(lowerer, info.typeParams, info.decl.typeParameters?.nodes, bindings); // A pinning signature that itself keeps type parameters (`let g: (x: // T) => T = identity` — storing the generic signature as such) binds // nothing: mapType answers null for an unsubstituted parameter. @@ -1076,13 +1082,13 @@ function runtimeOptionalHofGenericBinding( while (argument.parent && ts.isParenthesizedExpression(argument.parent)) argument = argument.parent; const call = argument.parent; - if (!ts.isCallExpression(call) || call.arguments[0] !== argument) return null; + if (!ts.isCallExpression(call) || call.arguments.nodes[0] !== argument) return null; if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== "map") return null; if (!lowerer.isStdlibMember(call.expression)) return null; const receiver = lowerer.mapTypeOf(lowerer.typeOf(call.expression.expression)); if (receiver?.kind !== "array") return null; - const parameter = info.decl.parameters[0]; + const parameter = info.decl.parameters.nodes[0]; if (!parameter || !ts.isIdentifier(parameter.name)) return null; const parameterType = lowerer.typeOf(parameter.name); if ((parameterType.flags & ts.TypeFlags.TypeParameter) === 0) return null; @@ -1138,7 +1144,7 @@ function genericValueInstance( try { const declSig = lowerer.checker.getSignatureFromDeclaration(info.decl); if (!declSig) lowerer.unsupported("SC1090", ref, "this function form"); - const params = paramShapes(lowerer, info.decl.parameters, declSig); + const params = paramShapes(lowerer, info.decl.parameters.nodes, declSig); const retTs = lowerer.checker.getReturnTypeOfSignature(declSig); const returnType = lowerer.mapTypeOf(retTs); if (!returnType) { @@ -1300,11 +1306,11 @@ export function implicitPatternsOf( lowerer: Lowerer, decl: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction, ): (ImplicitPattern | null)[] { - return decl.parameters.map((param): ImplicitPattern | null => { + return decl.parameters.nodes.map((param): ImplicitPattern | null => { if (!ts.isObjectBindingPattern(param.name) || !decl.body) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; const elements: ImplicitPattern["elements"] = []; - for (const element of param.name.elements) { + for (const element of param.name.elements.nodes) { if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) return null; const key = element.propertyName ?? element.name; @@ -1341,7 +1347,7 @@ function implicitPatternBinding( const values = new Map(); /** Shorthand properties name the binding they read. */ const shorthand = new Map(); - for (const property of literal.properties) { + for (const property of literal.properties.nodes) { if (ts.isShorthandPropertyAssignment(property) && ts.isIdentifier(property.name)) { values.set(property.name.text, property.name); const read = lowerer.checker.getShorthandAssignmentValueSymbol(property); @@ -1407,13 +1413,13 @@ export function implicitAnyParamSymbolsOf( ): (ts.Symbol | null)[] | null { if (!decl.body) return null; if (decl.asteriskToken) return null; - if (decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; + if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; if (decl.typeParameters !== undefined) return null; // real generics own the machinery - if (decl.parameters.length === 0) return null; + if (decl.parameters.nodes.length === 0) return null; // The variadic-`arguments` form keeps its dynRest story whole. if (bodyReadsArguments(decl)) return null; let hasImplicitParams = false; - const out = decl.parameters.map((param): ts.Symbol | null => { + const out = decl.parameters.nodes.map((param): ts.Symbol | null => { if (!ts.isIdentifier(param.name)) return null; if (param.dotDotDotToken || param.questionToken || param.initializer) return null; if (param.name.text === "this") return null; @@ -1456,7 +1462,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // Infer their result from the specialized body as well. if ( info.member?.kind === "static" && - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "object", @@ -1464,7 +1470,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | ) return null; const broadCallbackSymbols = new Set( - info.decl.parameters.flatMap((param, index) => { + info.decl.parameters.nodes.flatMap((param, index) => { const symbol = info.implicitParams?.[index]; return symbol !== null && symbol !== undefined && @@ -1507,7 +1513,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // container instead of checking every element against that guess. if (!hasExplicitJsDocReturn(info.decl)) return null; if ( - info.decl.parameters.some( + info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] && lowerer.mapTypeOf(lowerer.typeOf(param.name))?.kind === "array", @@ -1543,9 +1549,9 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | const symbol = lowerer.resolveValueSymbol(node.expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) openReturn = true; return undefined; @@ -1661,8 +1667,8 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if ( ts.isObjectLiteralExpression(arg) && isJsSourceFile(arg.getSourceFile()) && - (arg.properties.length === 0 || - arg.properties.some( + (arg.properties.nodes.length === 0 || + arg.properties.nodes.some( (property) => ts.isMethodDeclaration(property) || ts.isAccessor(property) || @@ -1726,9 +1732,9 @@ export function implicitCallInstance( /** The destructured names' bound types, which the object's own * representation does not always carry. */ const patternKey: string[] = []; - info.decl.parameters.forEach((param, i) => { + info.decl.parameters.nodes.forEach((param, i) => { const pattern = patterns[i]; - const patternArg = call.arguments[i]; + const patternArg = call.arguments.nodes[i]; if (pattern && patternArg && !ts.isSpreadElement(patternArg)) { // The argument's object type binds each destructured name, so a // class passed as `{ Base }` keeps its static class type. @@ -1751,7 +1757,7 @@ export function implicitCallInstance( let bound: IrType = DYN; const declared = lowerer.paramShape(param); if ((lowerer.typeOf(param.name).flags & ts.TypeFlags.Any) === 0) bound = declared.type; - const arg = call.arguments[i]; + const arg = call.arguments.nodes[i]; if (arg && !ts.isSpreadElement(arg)) { const stored = storedImplicitArgumentType(lowerer, arg); if (stored?.kind === "dyn") { @@ -1891,15 +1897,15 @@ export function implicitDefaultInstance( blame: ts.Node, info: GenericFnInfo, ): GenericInstance { - const shapes: ParamShape[] = info.decl.parameters.map((param, i) => + const shapes: ParamShape[] = info.decl.parameters.nodes.map((param, i) => info.implicitParams![i] ? { type: DYN, mode: "required" as const } : lowerer.paramShape(param), ); const argTypes = new Map(); // Newly bindable nominal class parameters must not retain their JSDoc // receiver type in the checked fallback. Keep existing broad JS inference. - for (let i = 0; i < info.decl.parameters.length; i++) { + for (let i = 0; i < info.decl.parameters.nodes.length; i++) { const symbol = info.implicitParams?.[i]; - const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters[i]!.name))?.kind; + const kind = lowerer.mapTypeOf(lowerer.typeOf(info.decl.parameters.nodes[i]!.name))?.kind; if (symbol && (kind === "object" || kind === "array")) { argTypes.set(symbol, lowerer.checker.getUnknownType()); } @@ -1935,7 +1941,7 @@ function internImplicitInstance( ); } const rendered = shapes.map((s) => lowerer.fmt(s.type)).join(", "); - const dynamicBroadPromise = info.decl.parameters.some( + const dynamicBroadPromise = info.decl.parameters.nodes.some( (param, index) => info.implicitParams?.[index] !== null && info.implicitParams?.[index] !== undefined && @@ -1976,7 +1982,9 @@ function internImplicitInstance( // this exact committed instance body here; repeated instantiations of // the same declaration find the facade memos warm. lowerer.checker.prefetchRoots([ - ...info.decl.parameters.flatMap((param) => (param.initializer ? [param.initializer] : [])), + ...info.decl.parameters.nodes.flatMap((param) => + param.initializer ? [param.initializer] : [], + ), ...(info.decl.body ? [info.decl.body] : []), ]); const fn = lowerer.lowerGenericInstance(info, inst); @@ -2033,9 +2041,9 @@ export function implicitLocalFnNodeOf( const sym = lowerer.checker.getSymbolAtLocation(decl.name); if (!sym) return null; const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0; - const redeclared = lowerer.checker - .declarationsOf(sym) - .some((d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined); + const redeclared = sym.declarations.some( + (d) => d !== decl && ts.isVariableDeclaration(d) && d.initializer !== undefined, + ); if (redeclared) return null; if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null; // The capture scan: instances are module functions with no frame. @@ -2051,7 +2059,7 @@ export function implicitLocalFnNodeOf( } if (ts.isIdentifier(n)) { const s = lowerer.checker.getSymbolAtLocation(n); - const d = s ? lowerer.checker.valueDeclarationOf(s) : undefined; + const d = s ? s.valueDeclaration : undefined; if ( d && d.getSourceFile() === decl.getSourceFile() && @@ -2138,7 +2146,7 @@ export function objLitGenericFnNodeOf( fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction; literal: ts.ObjectLiteralExpression; } | null { - const decl = lowerer.checker.valueDeclarationOf(propSym); + const decl = propSym.valueDeclaration; if (!decl) return null; if (ts.isMethodDeclaration(decl) && ts.isObjectLiteralExpression(decl.parent)) { return decl.typeParameters !== undefined && decl.body !== undefined @@ -2179,7 +2187,7 @@ export function objLitGenericFnInfoOf( const existing = lowerer.objLitGenericFns.get(fnNode); if (existing) return existing; if (fnNode.asteriskToken) lowerer.unsupported("SC1071", blame); - if (fnNode.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { + if (fnNode.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) { lowerer.unsupported("SC1090", blame, "async object-literal generic methods"); } // `this` is the receiver object — records don't model it (the @@ -2198,11 +2206,11 @@ export function objLitGenericFnInfoOf( } const typeParams = declaredTypeParameterSymbols( lowerer, - fnNode.typeParameters!, + fnNode.typeParameters!.nodes, blame, "this method form", ); - checkGenericParameterNames(lowerer, fnNode.parameters, false); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, false); const info: GenericFnInfo = { decl: fnNode, baseName: name, @@ -2312,7 +2320,12 @@ export function bindingGenericFnInfoOf( const typeParams: ts.Symbol[] = []; if (fnNode.typeParameters !== undefined) { typeParams.push( - ...declaredTypeParameterSymbols(lowerer, fnNode.typeParameters, fnNode, "this function form"), + ...declaredTypeParameterSymbols( + lowerer, + fnNode.typeParameters.nodes, + fnNode, + "this function form", + ), ); } else { // The CONTEXTUAL shape (bindingContextualGenericFnNodeOf): the type @@ -2328,7 +2341,7 @@ export function bindingGenericFnInfoOf( typeParams.push(tpSym); } } - checkGenericParameterNames(lowerer, fnNode.parameters, true); + checkGenericParameterNames(lowerer, fnNode.parameters.nodes, true); const stmt = decl.parent?.parent; // declarator → list → statement (nsPathPrefix wants the statement) const info: GenericFnInfo = { decl: fnNode, @@ -2407,7 +2420,7 @@ export function requireObjLitGenericReceiver( ); if (!ts.isIdentifier(recv)) fenceReceiver(); const recvSym = lowerer.resolveValueSymbol(recv); - const recvDecl = recvSym ? lowerer.checker.valueDeclarationOf(recvSym) : undefined; + const recvDecl = recvSym ? recvSym.valueDeclaration : undefined; if ( !recvDecl || !ts.isVariableDeclaration(recvDecl) || 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 812f29242..898c46f20 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -258,13 +258,134 @@ 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 @@ -368,7 +489,7 @@ export function lowerBytesIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "Array Iterator" }, loc, }); } @@ -474,7 +595,7 @@ export function lowerStringIteratorValue( }, { kind: "return", value: null, loc }, ], - generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType }, + generator: { yieldT: type.yieldT, nextT: UNDEFINED_T, resultType, tag: "String Iterator" }, loc, }); } @@ -635,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, }); } @@ -906,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..d39e5cceb 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, @@ -189,14 +190,14 @@ export function lowerValueReturningConstructorNew( if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = lowerer.checker.getAliasedSymbol(symbol); const decl = symbol - ? lowerer.checker - .declarationsOf(symbol) - .find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node)) + ? symbol.declarations.find( + (node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node), + ) : 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/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts new file mode 100644 index 000000000..01f632cf3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts @@ -0,0 +1,329 @@ +import { expect, test } from "vitest"; +import { F64, type IrExpr, type IrFunction, type IrStmt, type IrType } from "../../ir/ir.js"; +import { ShapeRegistry } from "../type-mapper.js"; +import { + applyKeyOrderWitnesses, + KeyOrderNotes, + type KeyOrderSegment, +} from "./key-order-witness.js"; +import type { Lowerer } from "./lowerer.js"; + +const loc = { file: "order.ts", start: 0, end: 1 }; +/** `number | undefined`: the optional-field slot type. */ +const OPT: IrType = { kind: "union", unionId: "u0" }; +const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); +const absent: IrExpr = { kind: "fieldAbsent", unionId: "u0", type: OPT, loc }; + +interface Fixture { + lowerer: Lowerer; + shapes: ShapeRegistry; + notes: KeyOrderNotes; + record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" }; + literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr; + run(body: IrStmt[]): void; + order(type: IrType & { kind: "record" }): string[] | undefined; +} + +function fixture(): Fixture { + const shapes = new ShapeRegistry(); + const notes = new KeyOrderNotes(); + const lowerer = { + shapes, + keyOrder: notes, + unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]), + classes: new Map(), + armTag: (unionId: string, arm: IrType): number => + unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1, + } as unknown as Lowerer; + const record = (fields: [string, IrType][], order: string[]): IrType & { kind: "record" } => ({ + kind: "record", + shapeId: shapes.intern( + [...fields].sort((a, b) => (a[0] < b[0] ? -1 : 1)).map(([name, type]) => ({ name, type })), + false, + undefined, + order, + ), + }); + const literal = (type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr => { + const shape = shapes.get((type as IrType & { kind: "record" }).shapeId)!; + const fields = keys.map((name) => ({ name, value: num(1) })); + for (const f of shape.fields) + if (!keys.includes(f.name)) fields.push({ name: f.name, value: absent }); + return notes.literal( + { kind: "recordLit", fields, type, loc }, + segments ?? keys.map((name) => ({ kind: "key", name, definite: true })), + ); + }; + const run = (body: IrStmt[]): void => { + const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc }; + applyKeyOrderWitnesses(lowerer, [fn]); + }; + const order = (type: IrType & { kind: "record" }): string[] | undefined => + shapes.get(type.shapeId)?.declaredOrder; + return { lowerer, shapes, notes, record, literal, run, order }; +} + +const observe = (value: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { kind: "jsonStringify", value, type: { kind: "string" }, loc }, + loc, +}); +const decl = (localId: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId, init, loc }); +const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc }); +const set = (obj: IrExpr, field: string): IrStmt => ({ + kind: "recordSet", + obj, + shapeId: (obj.type as IrType & { kind: "record" }).shapeId, + field, + value: num(2), + loc, +}); + +test("a shape takes the one order its literals write", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["b", "a"]); + expect(f.notes.changed.has(ab.shapeId)).toBe(true); +}); + +test("literals that agree with the table leave the same array in place", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const before = f.order(ab); + f.run([observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toBe(before); + expect(f.notes.changed.size).toBe(0); +}); + +test("values built in two orders keep today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([observe(f.literal(ab, ["b", "a"])), observe(f.literal(ab, ["a", "b"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("an unobserved shape keeps today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("x", f.literal(ab, ["b", "a"]))]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a synthesized construction pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const synthesized: IrExpr = { + kind: "recordLit", + fields: [ + { name: "a", value: num(1) }, + { name: "b", value: num(2) }, + ], + type: ab, + loc, + }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(synthesized)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a checked conversion pins today's table", () => { + const f = fixture(); + const ab = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc }; + f.run([observe(f.literal(ab, ["b", "a"])), observe(parsed)]); + expect(f.order(ab)).toEqual(["a", "b"]); +}); + +test("a spread contributes its source's order, and an override keeps its position", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["a", "b", "c"], + ); + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", f.literal(src, ["b", "a"])), + // { ...s, a: 1, c: 2 } — `a` stays where the spread put it. + observe( + f.literal( + out, + ["a", "b", "c"], + [ + spread, + { kind: "key", name: "a", definite: true }, + { kind: "key", name: "c", definite: true }, + ], + ), + ), + ]); + expect(f.order(out)).toEqual(["b", "a", "c"]); +}); + +test("a spread of a checked-conversion source cannot be ordered statically", () => { + const f = fixture(); + const src = f.record( + [ + ["a", F64], + ["b", F64], + ], + ["a", "b"], + ); + const out = f.record( + [ + ["a", F64], + ["b", F64], + ["c", F64], + ], + ["c", "a", "b"], + ); + const parsed: IrExpr = { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: src, loc }; + const spread: KeyOrderSegment = { kind: "spread", shapeId: src.shapeId, maybeAbsent: false }; + f.run([ + decl("s", parsed), + observe(f.literal(out, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }])), + ]); + expect(f.order(out)).toEqual(["c", "a", "b"]); +}); + +test("a late-only optional key follows the keys values are built with", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + f.run([decl("o", f.literal(o, ["b"])), set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["b", "a"]); +}); + +test("straight-line writes of late-only keys keep program order", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("o", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "y", "x"]); +}); + +test("late-only keys written through different bindings keep today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + set(ref("p", o), "x"), + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("a late-only key written in a loop over an outer binding keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["id", F64], + ["x", OPT], + ["y", OPT], + ], + ["id", "x", "y"], + ); + f.run([ + decl("o", f.literal(o, ["id"])), + set(ref("o", o), "y"), + { + kind: "while", + cond: { kind: "boolLit", value: false, type: { kind: "bool" }, loc }, + body: [set(ref("o", o), "x")], + loc, + }, + observe(ref("o", o)), + ]); + expect(f.order(o)).toEqual(["id", "x", "y"]); +}); + +test("re-adding a key a literal creates earlier keeps today's table", () => { + const f = fixture(); + const o = f.record( + [ + ["a", OPT], + ["b", F64], + ], + ["a", "b"], + ); + const del: IrStmt = { + kind: "recordSet", + obj: ref("o", o), + shapeId: o.shapeId, + field: "a", + value: absent, + loc, + }; + f.run([decl("o", f.literal(o, ["a", "b"])), del, set(ref("o", o), "a"), observe(ref("o", o))]); + expect(f.order(o)).toEqual(["a", "b"]); +}); diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts new file mode 100644 index 000000000..ba8001792 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts @@ -0,0 +1,675 @@ +/* Record enumeration order chosen from the orders values are built in. + * + * Node enumerates an object's string keys as canonical array indices + * ascending, then every other key in creation order. A record stores its + * fields name-sorted and enumerates them through one per-shape table, + * `IrRecordShape.declaredOrder`, which the type mapper fills with the + * checker's property order of the first type interned to the shape. That + * table is exact only while every value of the shape is created in that + * order. This pass replaces it, after lowering, with the order the + * program's construction sites actually create, when that order is one + * order. It changes the table's contents only: storage layout, shape + * identity and every helper's structure stay as they are, so the generated + * code differs only in the constant order the enumeration surfaces walk. + * + * Witnesses, per shape: + * - an object literal contributes the order it writes its keys in. Spread + * segments contribute their source shape's order (an inheritance edge, + * never the desugar's field list), and a key keeps the position of its + * first occurrence, so an override inside a spread stays where the + * spread put it. A key whose earlier occurrence may be absent at run + * time contributes every position it can take; + * - a width copy or a rest binding contributes its source shape's order; + * - any other construction (synthesized builtin results, opaque runtime + * producers) pins today's table; + * - checked conversions from dynamic values (typed JSON.parse, values + * from JavaScript) pin today's table: the record-source table keeps + * their own key order relative to it, and another table would make + * values parsed in today's order keep a source view. Their order is + * unknown, so a copy out of such a shape cannot be ordered statically; + * - a write to an optional key that may be absent appends it in Node. A + * key no construction creates ("late-only") goes after every key a + * construction can create; any other moving write goes after every key. + * Two late-only keys are ordered only by a straight-line sequence of + * writes through one binding. + * + * The orders merge into a precedence graph per shape, topologically sorted + * with ties broken by today's table, so a shape whose witnesses agree with + * the table keeps it untouched. A cycle means the order depends on the + * value: that shape, and every shape copied from it, keeps today's table + * (and today's SC6002 warnings). Shapes that no order surface observes + * keep today's table too. */ +import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir/ir.js"; +import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js"; +import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; + +/** One property of an object literal, in source order. */ +export type KeyOrderSegment = + /** An explicit key (`definite`), or a conditional spread's key, which + * exists only when its condition holds. */ + | { kind: "key"; name: string; definite: boolean } + /** `...source` of a record shape; `maybeAbsent` when the whole source + * can be undefined (`...options` of `T | undefined`). */ + | { kind: "spread"; shapeId: string; maybeAbsent: boolean } + /** A spread whose keys are only known at run time. */ + | { kind: "opaque" }; + +/** Witness notes the lowering attaches to the IR nodes it builds. Keyed by + * node identity, so nodes a speculative lowering discards are never walked, + * and a node some later pass rebuilds falls back to pinning. */ +export class KeyOrderNotes { + private readonly literals = new Map(); + private readonly copies = new Map(); + /** Shapes a frontend order surface (keys, values/entries, inspect, + * index-signature merges) walks. */ + readonly observed = new Set(); + /** Shapes whose table this pass replaced. */ + readonly changed = new Set(); + + literal(node: IrExpr, segments: KeyOrderSegment[]): IrExpr { + this.literals.set(node, segments); + return node; + } + + copy(node: IrExpr, sourceShapeId: string): IrExpr { + this.copies.set(node, sourceShapeId); + return node; + } + + observe(type: IrType): void { + if (type.kind === "record") this.observed.add(type.shapeId); + } + + literalOf(node: IrExpr): KeyOrderSegment[] | undefined { + return this.literals.get(node); + } + + copyOf(node: IrExpr): string | undefined { + return this.copies.get(node); + } +} + +const isArrayIndex = (name: string): boolean => { + const n = Number(name); + return Number.isInteger(n) && n >= 0 && n < 4294967295 && String(n) === name; +}; + +interface WriteSite { + field: string; + kind: "write" | "delete" | "clone"; + fn: string; + binding: string | null; + loop: string; + pos: number; +} + +interface BindingInit { + count: number; + fn: string; + loop: string; + pos: number; + fresh: boolean; +} + +interface ShapeFacts { + literals: KeyOrderSegment[][]; + copies: { source: string; keys: Set }[]; + /** A construction in today's order (synthesized literal). */ + pinned: boolean; + /** An opaque runtime producer: today's order, any key may be absent. */ + opaque: boolean; + /** A checked conversion from a dynamic value. */ + runtime: boolean; + clones: boolean; + present: Set; + absent: Set; + deleted: Set; + writes: WriteSite[]; +} + +type Resolved = { order: string[] } | "unknown"; + +/** `structuredClone(v)` checked back into v's own type: the copy is built + * from v's dyn view, so it creates exactly v's keys in v's order. */ +function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean { + const value = node.value; + if (value.kind !== "libCall" || value.fn !== "dyn.structuredClone") return false; + const source = value.args[0]; + return ( + source !== undefined && source.kind === "dynFrom" && typeEquals(source.value.type, node.type) + ); +} + +/** Chooses each in-scope record shape's enumeration table from its + * witnesses. Runs once retained reachability has settled the historical + * tables and derived metadata, before the order-snapshotting helper bodies + * rebuild. */ +export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunction[]): void { + const notes = lowerer.keyOrder; + const shapes = lowerer.shapes; + const facts = new Map(); + const factsOf = (shapeId: string): ShapeFacts => { + let f = facts.get(shapeId); + if (f === undefined) { + f = { + literals: [], + copies: [], + pinned: false, + opaque: false, + runtime: false, + clones: false, + present: new Set(), + absent: new Set(), + deleted: new Set(), + writes: [], + }; + facts.set(shapeId, f); + } + return f; + }; + const observedRoots: IrType[] = []; + const inits = new Map(); + const undefinedArmed = (t: IrType): boolean => + t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0; + const containsAbsent = (value: IrExpr): boolean => + value.kind === "fieldAbsent" || + (value.kind === "ternary" && (containsAbsent(value.then) || containsAbsent(value.else_))); + + /** Every record shape a type can hold, through fields, arms, elements, + * callables, promises and class fields. */ + /** Each walk shares one `seen` set per result set, so every type graph + * is walked once however many roots reach it. */ + const shapesIn = (type: IrType, out: Set, seen: Set): void => { + const visit = (t: IrType): boolean => { + if (t.kind === "record") { + if (seen.has("r:" + t.shapeId)) return true; + seen.add("r:" + t.shapeId); + out.add(t.shapeId); + const shape = shapes.get(t.shapeId); + if (shape) { + for (const f of shape.fields) visit(f.type); + if (shape.indexValue) visit(shape.indexValue); + } + return true; + } + if (t.kind === "union") { + if (seen.has("u:" + t.unionId)) return true; + seen.add("u:" + t.unionId); + for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm); + return true; + } + if (t.kind === "object") { + if (seen.has("c:" + t.className)) return true; + seen.add("c:" + t.className); + for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type); + return true; + } + return everyTypeChild(t, visit); + }; + visit(type); + }; + const opaqueShapes = new Set(); + const opaqueSeen = new Set(); + const runtimeShapes = new Set(); + const runtimeSeen = new Set(); + const producedBy = (type: IrType, opaque: boolean): void => { + if (opaque) shapesIn(type, opaqueShapes, opaqueSeen); + else shapesIn(type, runtimeShapes, runtimeSeen); + }; + + // ── collection ────────────────────────────────────────────────────── + let pos = 0; + let fnName = ""; + const loops: number[] = []; + let loopIds = 0; + const loopKey = (): string => loops.join("/"); + const noteInit = (id: string, init: IrExpr): void => { + const previous = inits.get(id); + if (previous !== undefined) { + previous.count++; + return; + } + const fresh = + init.kind === "recordLit" || + init.kind === "recordClone" || + (init.kind === "seqExpr" && init.result.kind === "recordLit"); + inits.set(id, { count: 1, fn: fnName, loop: loopKey(), pos, fresh }); + }; + const visitExpr = (e: IrExpr): void => { + switch (e.kind) { + case "recordLit": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + const presentKeys = new Set(); + for (const field of e.fields) { + if (field.overflow) continue; + if (field.value.kind === "fieldAbsent") { + f.absent.add(field.name); + continue; + } + presentKeys.add(field.name); + f.present.add(field.name); + if (containsAbsent(field.value)) f.absent.add(field.name); + } + const segments = notes.literalOf(e); + const copy = notes.copyOf(e); + if (segments !== undefined) f.literals.push(segments); + else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys }); + else f.pinned = true; + return; + } + case "recordClone": { + if (e.type.kind !== "record") return; + const f = factsOf(e.type.shapeId); + f.clones = true; + for (const o of e.overrides) + f.writes.push({ field: o.name, kind: "clone", fn: fnName, binding: null, loop: "", pos }); + return; + } + case "assignExpr": + noteInit(e.localId, e.value); + return; + case "jsonStringify": + case "dynFrom": + observedRoots.push(e.value.type); + return; + case "recordKeyGet": + observedRoots.push(e.obj.type); + return; + case "libCall": + // Runtime surfaces (console, util.format, workers) that take a + // record may walk its keys; runtime builders make new records. + for (const arg of e.args) observedRoots.push(arg.type); + producedBy(e.type, true); + return; + case "intrinsic": + // Intrinsics hand back values built elsewhere, except allSettled's + // result records; console output renders through the inspect + // helpers, which note their shapes. + if (e.name === "promise.allSettled") producedBy(e.type, true); + return; + case "ffiCall": + case "strIntrinsic": + case "regexIntrinsic": + case "bytesIntrinsic": + producedBy(e.type, true); + return; + case "dynCheck": + if (!orderPreservingClone(e)) producedBy(e.type, false); + return; + case "dynDestrCheck": + case "caughtCheck": + case "caughtNarrow": + producedBy(e.type, false); + return; + default: + return; + } + }; + const visitStmt = (st: IrStmt): void => { + switch (st.kind) { + case "recordSet": + if (st.value.kind === "fieldAbsent") factsOf(st.shapeId).deleted.add(st.field); + factsOf(st.shapeId).writes.push({ + field: st.field, + kind: st.value.kind === "fieldAbsent" ? "delete" : "write", + fn: fnName, + binding: st.obj.kind === "varRef" ? st.obj.localId : null, + loop: loopKey(), + pos, + }); + return; + case "varDecl": + if (st.init !== null) noteInit(st.localId, st.init); + return; + case "assign": + noteInit(st.localId, st.value); + return; + default: + return; + } + }; + const expr = (e: IrExpr): boolean => { + pos++; + visitExpr(e); + return everyExprChild(e, expr, stmt); + }; + const stmt = (st: IrStmt): boolean => { + pos++; + visitStmt(st); + if (st.kind === "while" || st.kind === "doWhile" || st.kind === "for" || st.kind === "forOf") { + loops.push(++loopIds); + everyStmtChild(st, expr, stmt); + loops.pop(); + return true; + } + return everyStmtChild(st, expr, stmt); + }; + for (const fn of fns) { + fnName = fn.name; + for (const st of fn.body) stmt(st); + } + for (const id of opaqueShapes) { + const f = factsOf(id); + f.pinned = true; + f.opaque = true; + } + for (const id of runtimeShapes) factsOf(id).runtime = true; + + // ── scope ─────────────────────────────────────────────────────────── + // Only plain declared-field shapes with a table take part. + const inScope = (shape: IrRecordShape | undefined): shape is IrRecordShape => + shape !== undefined && + !shape.tuple && + shape.indexValue === undefined && + shape.declaredOrder !== undefined && + !shapeHasAccessorSlots(shape); + const tableOf = (shape: IrRecordShape): string[] => + shape.declaredOrder!.filter((k) => !k.startsWith("%") && !isArrayIndex(k)); + // A checked conversion builds values in their source's own order, which + // the record-source table reproduces relative to the shape's table. A new + // table would make every value parsed in today's order keep a source + // view, so such shapes keep today's table. + for (const f of facts.values()) if (f.runtime) f.pinned = true; + + // Nothing to choose: every witness agrees with today's table and no + // optional key is written late. + const interesting = [...facts].some(([id, f]) => { + const shape = shapes.get(id); + if (!inScope(shape)) return false; + if (f.writes.some((w) => w.kind !== "delete") || f.copies.length > 0) return true; + const table = tableOf(shape).join("\0"); + return f.literals.some((segments) => { + const keys: string[] = []; + for (const segment of segments) { + if (segment.kind !== "key") return true; + if (!isArrayIndex(segment.name)) keys.push(segment.name); + } + return keys.join("\0") !== table; + }); + }); + if (!interesting) return; + + // ── presence classes and late-write constraints ───────────────────── + const maybeAbsent = (id: string, key: string): boolean => { + const shape = shapes.get(id); + const field = shape?.fields.find((x) => x.name === key); + if (!field || !undefinedArmed(field.type)) return false; + const f = facts.get(id); + if (f === undefined) return true; + return f.absent.has(key) || f.runtime || f.opaque || f.deleted.has(key); + }; + /** Edges every value of the shape satisfies whatever its sources' + * orders are, or "unknown" when late writes make the order depend on + * the value. */ + const lateEdges = (id: string, keys: readonly string[]): [string, string][] | "unknown" => { + const f = facts.get(id); + if (f === undefined) return []; + const edges: [string, string][] = []; + const keySet = new Set(keys); + const createdKeys = (k: string): boolean => f.present.has(k) || f.runtime || f.opaque; + const required = keys.filter((k) => !maybeAbsent(id, k)); + const created = keys.filter((k) => maybeAbsent(id, k) && createdKeys(k)); + const lateOnly = new Set(keys.filter((k) => maybeAbsent(id, k) && !createdKeys(k))); + // The constraint depends on the written key only, not on the site. + const moving = new Set(); + for (const w of f.writes) + if (w.kind !== "delete" && keySet.has(w.field) && maybeAbsent(id, w.field)) + moving.add(w.field); + for (const field of moving) { + const before = lateOnly.has(field) ? [...required, ...created] : keys; + for (const k of before) if (k !== field) edges.push([k, field]); + } + const lateSites = f.writes.filter((w) => lateOnly.has(w.field)); + const lateWritten = new Set(lateSites.filter((w) => w.kind !== "delete").map((w) => w.field)); + if (lateWritten.size < 2) return edges; + // Two or more late-only keys: each value gains them in the order its + // writes run, so every write must belong to one straight-line + // sequence through one binding initialized once, in the same function + // and loop body as its writes. + const groups = new Map(); + for (const w of lateSites) { + if (w.kind === "clone" || w.binding === null) return "unknown"; + const init = inits.get(w.binding); + if ( + init === undefined || + init.count !== 1 || + init.fn !== w.fn || + init.loop !== w.loop || + init.pos > w.pos + ) + return "unknown"; + const group = groups.get(w.binding) ?? []; + group.push(w); + groups.set(w.binding, group); + } + if (groups.size > 1 && [...groups.keys()].some((b) => !inits.get(b)!.fresh)) return "unknown"; + for (const group of groups.values()) { + const seen = new Set(); + for (const w of group) { + if (seen.has(w.field)) return "unknown"; + seen.add(w.field); + } + const writes = group.filter((w) => w.kind === "write").sort((a, b) => a.pos - b.pos); + for (let i = 0; i + 1 < writes.length; i++) + edges.push([writes[i]!.field, writes[i + 1]!.field]); + } + return edges; + }; + + // ── witness sequences ─────────────────────────────────────────────── + /** The key sequences one literal can create, or null when a spread + * source's order is unknown. A key sits at its first occurrence that + * certainly defines it; each earlier occurrence that may define it is + * another possible position. */ + const literalSequences = ( + segments: readonly KeyOrderSegment[], + keySet: ReadonlySet, + resolved: (id: string) => Resolved, + ): string[][] | null => { + const occurrences: { key: string; definite: boolean }[] = []; + for (const s of segments) { + if (s.kind === "opaque") return null; + if (s.kind === "key") { + if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite }); + continue; + } + const source = resolved(s.shapeId); + if (source === "unknown") return null; + for (const k of source.order) { + if (keySet.has(k)) + occurrences.push({ key: k, definite: !s.maybeAbsent && !maybeAbsent(s.shapeId, k) }); + } + } + // Per key, the occurrences that can create it: every one up to and + // including the first definite occurrence. The first such position is + // the base; each other one is an alternative. + const positions = new Map(); + const settled = new Set(); + occurrences.forEach((o, i) => { + if (settled.has(o.key)) return; + const list = positions.get(o.key) ?? []; + list.push(i); + positions.set(o.key, list); + if (o.definite) settled.add(o.key); + }); + const base = new Map(); + for (const [key, list] of positions) base.set(key, list[0]!); + const sequenceWith = (at: Map): string[] => + [...at].sort((a, b) => a[1] - b[1]).map(([k]) => k); + const out = [sequenceWith(base)]; + for (const [key, list] of positions) { + for (const alternative of list.slice(1)) { + const moved = new Map(base); + moved.set(key, alternative); + out.push(sequenceWith(moved)); + } + } + return out; + }; + + /** Today's table with every witness edge applied, or "unknown". */ + const merge = (table: readonly string[], edges: readonly [string, string][]): Resolved => { + const rank = new Map(table.map((k, i) => [k, i])); + const succ = new Map>(table.map((k) => [k, new Set()])); + const indeg = new Map(table.map((k) => [k, 0])); + for (const [a, b] of edges) { + if (a === b || !rank.has(a) || !rank.has(b)) continue; + const s = succ.get(a)!; + if (s.has(b)) continue; + s.add(b); + indeg.set(b, indeg.get(b)! + 1); + } + const order: string[] = []; + const ready = table.filter((k) => indeg.get(k) === 0); + while (ready.length > 0) { + ready.sort((a, b) => rank.get(a)! - rank.get(b)!); + const k = ready.shift()!; + order.push(k); + for (const next of succ.get(k)!) { + indeg.set(next, indeg.get(next)! - 1); + if (indeg.get(next) === 0) ready.push(next); + } + } + return order.length === table.length ? { order } : "unknown"; + }; + + const chain = (seq: readonly string[], out: [string, string][]): void => { + for (let i = 0; i + 1 < seq.length; i++) out.push([seq[i]!, seq[i + 1]!]); + }; + + // ── resolution over inheritance edges ─────────────────────────────── + const result = new Map(); + const late = new Map(); + const dependencies = (id: string): string[] => { + const f = facts.get(id); + if (f === undefined) return []; + const deps = new Set(); + for (const segments of f.literals) + for (const s of segments) if (s.kind === "spread") deps.add(s.shapeId); + for (const c of f.copies) deps.add(c.source); + return [...deps]; + }; + /** One shape's order given its sources' current orders. */ + const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => { + const shape = shapes.get(id); + if (!inScope(shape)) return "unknown"; + const table = tableOf(shape); + const f = facts.get(id); + // A shape the program never constructs natively gets its values from + // somewhere this pass cannot see. + if (f === undefined || !(f.literals.length > 0 || f.copies.length > 0 || f.pinned || f.runtime)) + return "unknown"; + // Clones copy their source's own key order, which a checked + // conversion leaves unknown. + if (f.runtime && f.clones) return "unknown"; + const keySet = new Set(table); + const edges: [string, string][] = []; + if (f.pinned) chain(table, edges); + for (const segments of f.literals) { + const sequences = literalSequences(segments, keySet, current); + if (sequences === null) return "unknown"; + for (const seq of sequences) chain(seq, edges); + } + for (const c of f.copies) { + const source = current(c.source); + if (source === "unknown") return "unknown"; + chain( + source.order.filter((k) => keySet.has(k) && c.keys.has(k)), + edges, + ); + } + const cached = late.get(id); + const lateFor = cached !== undefined ? cached : lateEdges(id, table); + if (cached === undefined) late.set(id, lateFor); + if (lateFor === "unknown") return "unknown"; + for (const edge of lateFor) edges.push(edge); + return merge(table, edges); + }; + /** A source's order as its copies see it: values of a shape built by a + * checked conversion keep their source's own order. */ + const asSource = (id: string, resolved: Resolved): Resolved => + facts.get(id)?.runtime === true ? "unknown" : resolved; + + // Tarjan's SCCs over "depends on" edges: sources finish first. + const index = new Map(); + const low = new Map(); + const stack: string[] = []; + const onStack = new Set(); + let counter = 0; + const sccs: string[][] = []; + const strong = (v: string): void => { + index.set(v, counter); + low.set(v, counter); + counter++; + stack.push(v); + onStack.add(v); + for (const w of dependencies(v)) { + if (!index.has(w)) { + strong(w); + low.set(v, Math.min(low.get(v)!, low.get(w)!)); + } else if (onStack.has(w)) low.set(v, Math.min(low.get(v)!, index.get(w)!)); + } + if (low.get(v) === index.get(v)) { + const scc: string[] = []; + let w: string; + do { + w = stack.pop()!; + onStack.delete(w); + scc.push(w); + } while (w !== v); + sccs.push(scc); + } + }; + for (const id of facts.keys()) if (!index.has(id)) strong(id); + const external = (id: string): Resolved => { + const known = result.get(id); + return known === undefined ? "unknown" : asSource(id, known); + }; + for (const scc of sccs) { + const members = new Set(scc); + const guess = new Map(); + for (const id of scc) { + const shape = shapes.get(id); + guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown"); + } + const lookup = (id: string): Resolved => + members.has(id) ? asSource(id, guess.get(id)!) : external(id); + let stable = false; + for (let round = 0; round < scc.length + 3 && !stable; round++) { + stable = true; + for (const id of scc) { + const next = resolveOne(id, lookup); + const prev = guess.get(id)!; + const same = + next === "unknown" + ? prev === "unknown" + : prev !== "unknown" && prev.order.join("\0") === next.order.join("\0"); + if (!same) { + stable = false; + guess.set(id, next); + } + } + } + for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown"); + } + + // ── observation ───────────────────────────────────────────────────── + const observed = new Set(); + const observedSeen = new Set(); + for (const id of notes.observed) + shapesIn({ kind: "record", shapeId: id }, observed, observedSeen); + for (const root of observedRoots) shapesIn(root, observed, observedSeen); + + // ── apply ─────────────────────────────────────────────────────────── + for (const [id, resolved] of result) { + if (resolved === "unknown" || !observed.has(id)) continue; + const shape = shapes.get(id); + if (!inScope(shape)) continue; + const table = tableOf(shape); + if (resolved.order.join("\0") === table.join("\0")) continue; + shape.declaredOrder = [...shape.declaredOrder!.filter(isArrayIndex), ...resolved.order]; + notes.changed.add(id); + } +} 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..cb31ecc79 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, @@ -870,7 +870,7 @@ export function lowerAssertDirectCall( const module = lowerer.builtinNamespaceModuleOf(callee); if (module !== "assert" && module !== "assert/strict") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", @@ -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-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts index c05430ad3..5e04ee83c 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts @@ -132,13 +132,11 @@ export function isNativeBuiltinValueInitializer( const symbol = lowerer.resolveValueSymbol(expr); if (!symbol || seen.has(symbol)) return false; seen.add(symbol); - return lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isVariableDeclaration(decl) && - isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), - ); + return symbol.declarations.some( + (decl) => + ts.isVariableDeclaration(decl) && + isNativeBuiltinValueInitializer(lowerer, decl.initializer, seen), + ); } /** The stored Array.isArray function shares the direct call's native test. */ diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 58cbb9543..707b60481 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -65,6 +65,7 @@ import { isUnitType, shapeHasAccessorSlots, typeEquals, + RUNTIME_EMITTER_CLASS, } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; @@ -113,6 +114,9 @@ import { lowerStringMethodCall, lowerStringPaddingCall, lowerStringSplitCall, + isDynPrimitiveKindMethod, + lowerDynPrimitiveMethodCall, + lowerExtraNumberMethodCall, } from "./containers/string-and-regexp.js"; import { createRequireSpecOf } from "./builtins/module-bindings.js"; import { @@ -125,7 +129,9 @@ import { lowerRequireResolveCall, } from "./builtins/module-resolution.js"; import { + isWebCryptoReceiver, lowerCipherMethodCall, + lowerCryptoGetRandomValues, lowerCryptoHashMethodCall, lowerPromisifiedCryptoCall, } from "./builtins/crypto.js"; @@ -162,6 +168,7 @@ 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 { structuralReadType } from "./class-unions.js"; import { httpClientFnBindingOf, lowerCompatReqStreamOptionalCall, @@ -247,7 +254,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; } @@ -536,7 +543,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 { @@ -547,7 +554,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 }; @@ -587,11 +594,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); @@ -606,16 +613,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 && @@ -625,11 +632,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 ? [] @@ -655,7 +662,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)); @@ -679,12 +686,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { }; // The declared result is the value's own type (the generic's T): // validate the dyn copy back into it when the type can be checked; - // dyn-typed and unmappable results stay dyn values (JS files). + // dyn-typed and unmappable results stay dyn values (JS files). An + // empty record (`{}` inferred from a JavaScript `payload = {}` + // default) is the checker's supertype of whatever arrives, not a + // layout: checking into it would drop every member of the copy. const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const resultShape = + resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined; + const emptyRecord = + resultShape !== undefined && + !resultShape.tuple && + resultShape.indexValue === undefined && + resultShape.fields.length === 0; if ( resultT !== null && resultT.kind !== "dyn" && resultT.kind !== "void" && + !emptyRecord && canDynCheckTo( resultT, (id) => lowerer.shapes.get(id), @@ -718,10 +736,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); @@ -744,10 +762,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 }; @@ -814,10 +832,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 { @@ -851,7 +869,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 @@ -899,9 +917,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); @@ -932,10 +950,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")) { @@ -972,7 +990,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; @@ -995,14 +1013,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); @@ -1037,13 +1055,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") { @@ -1070,20 +1088,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`, @@ -1103,8 +1121,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; @@ -1133,7 +1151,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). @@ -1145,18 +1164,21 @@ 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; + const declaration = symbol ? symbol.valueDeclaration : undefined; // A bare call supplies undefined as this, even inside a callback // currently carrying a receiver. The value-call ABI saves and // restores that ambient receiver around the function invocation. if ( declaration && ts.isFunctionDeclaration(declaration) && - declaration.parameters.some(isThisParameter) + declaration.parameters.nodes.some(isThisParameter) ) { const callee = lowerer.lowerExpr(expr.expression); return reconcileOverloadReturn(lowerer, expr, { @@ -1249,7 +1271,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, @@ -1312,7 +1334,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, @@ -1484,6 +1506,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? + // crypto.getRandomValues on the WebCrypto global (untyped in + // fallback builds, so before the dyn receiver path) or webcrypto. + lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -1531,13 +1556,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), }; @@ -1550,8 +1575,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, @@ -1627,7 +1655,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ts.isElementAccessExpression(expr.expression) && !expr.expression.questionDotToken && !expr.questionDotToken && - expr.arguments.length === 0 + expr.arguments.nodes.length === 0 ) { const iterator = lowerStringIteratorCall(lowerer, expr, expr.expression); if (iterator) return iterator; @@ -1679,7 +1707,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") @@ -1692,7 +1720,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, }; @@ -1738,7 +1766,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, @@ -1793,7 +1826,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. @@ -1801,7 +1834,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) @@ -1820,7 +1853,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, }; @@ -1841,7 +1874,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; } @@ -1853,17 +1886,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 }; @@ -1886,14 +1919,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"); @@ -1902,7 +1936,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, }; @@ -1917,18 +1951,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]!)], }; } @@ -1951,7 +1986,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 || !( @@ -1963,7 +1998,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", ); } @@ -1975,7 +2010,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", ); } @@ -2048,7 +2083,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; @@ -2274,7 +2309,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 = @@ -2345,7 +2380,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 } }; @@ -2356,7 +2391,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) }; @@ -2387,7 +2422,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 = @@ -2454,7 +2489,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; @@ -2469,10 +2504,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", @@ -2501,15 +2536,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, ); } @@ -2524,7 +2559,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)) { @@ -2535,8 +2570,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, ); @@ -2790,17 +2825,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, @@ -2815,12 +2851,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 }; } @@ -2855,7 +2897,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; @@ -2881,7 +2923,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 { @@ -2910,7 +2952,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") @@ -2933,12 +2975,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); @@ -2952,7 +2994,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 @@ -2987,7 +3032,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"); @@ -2998,7 +3043,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) { @@ -3014,7 +3059,7 @@ function lowerStringPrototypeCall( indexMethod, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); if (!entry) return null; if (padding) { @@ -3024,7 +3069,7 @@ function lowerStringPrototypeCall( entry.method as "padStart" | "padEnd", receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } if (entry.method === "split") { @@ -3033,7 +3078,7 @@ function lowerStringPrototypeCall( call, receiverValue, receiverNode, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } const receiver = lowerer.ensureString(receiverValue, receiverNode); @@ -3042,7 +3087,7 @@ function lowerStringPrototypeCall( call, methodAccess, () => receiver, - call.arguments.slice(1), + call.arguments.nodes.slice(1), ); } @@ -3317,7 +3362,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 { @@ -3328,7 +3373,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, }, @@ -3391,6 +3436,23 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, () => recv) ); } + // A checker-untyped JS binding holding `string | undefined` (string- + // width's `const visibleSegment = baseVisible(segment)` after an + // `=== undefined` guard the checker cannot see through): no typed + // lowering claims the union, so a String method boxes it and takes the + // dyn receiver path, whose undefined/null arms throw Node's read errors. + if ( + checkerUntyped && + recv.type.kind === "union" && + isJsSourceFile(call.getSourceFile()) && + isStringMethodName(access.name.text) && + lowerer.unions + .get(recv.type.unionId) + ?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) === true && + lowerer.dynConvertible(recv.type) + ) { + recv = lowerer.coerceToExpected(recv, DYN); + } // A native handle read through `as any` (`(hash as any).update(x)`) // dispatches through the handle, as the same value held in an // 'unknown' slot does. @@ -3399,11 +3461,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); @@ -3467,8 +3529,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, @@ -3487,9 +3549,9 @@ function lowerDynReceiverMethodCall( // Those are not stored members of a checked-dynamic object, so // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only // an authored declaration can justify the own-member path. - const authored = lowerer.checker - .declarationsOf(prop) - .some((decl) => !lowerer.isStdlibFile(decl.getSourceFile())); + const authored = prop.declarations.some( + (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + ); return ( authored && lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(prop)).length > 0 @@ -3515,10 +3577,30 @@ function lowerDynReceiverMethodCall( lowerStringMethodCall(lowerer, call, access, checked) ); } + // String/Number methods the runtime dispatch has no answer for + // (codePointAt, substr, toLocaleLowerCase, toPrecision, ...): a string + // or number receiver runs the typed lowering, every other kind keeps the + // runtime dispatch. + if ( + !ownCallable && + !call.questionDotToken && + !access.questionDotToken && + isDynPrimitiveKindMethod(access.name.text) && + !lowerer.chainBlocked(access, call) + ) { + const primitive = lowerDynPrimitiveMethodCall( + lowerer, + call, + access.name.text, + access.getText(), + recv, + ); + if (primitive) return primitive; + } // 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", @@ -3534,7 +3616,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", @@ -3570,9 +3652,9 @@ function lowerDynReceiverMethodCall( return true; return ( prop !== undefined && - lowerer.checker - .declarationsOf(prop) - .some((decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) + prop.declarations.some( + (decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl), + ) ); })(); // Protocol methods keep their static signature: instances of the @@ -3650,13 +3732,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, @@ -3958,7 +4040,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); @@ -3988,11 +4070,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)", ); } @@ -4004,15 +4088,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 = @@ -4022,7 +4106,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 && @@ -4048,7 +4132,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 { @@ -4062,7 +4146,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), }, @@ -4072,7 +4156,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 @@ -4090,6 +4174,19 @@ export function lowerDynDispatchMethodCall( }; } +/** Every String.prototype method name a typed or dyn string lowering + * answers. */ +function isStringMethodName(name: string): boolean { + return ( + Object.hasOwn(STR_METHODS, name) || + STRING_INDEX_METHODS.has(name) || + DYN_STRING_ONLY_METHODS.has(name) || + isDynPrimitiveKindMethod(name) || + name === "normalize" || + name === "lastIndexOf" + ); +} + /** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any * other dyn kind's prototype) also declares. */ const DYN_STRING_ONLY_METHODS = new Set([ @@ -4128,8 +4225,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") { @@ -4212,17 +4309,15 @@ function lowerCaughtPredicateCall( const callee = call.expression; if (!ts.isIdentifier(callee)) return null; const symbol = lowerer.resolveValueSymbol(callee); - const decl = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isFunctionDeclaration) - : undefined; + const decl = symbol ? symbol.declarations.find(ts.isFunctionDeclaration) : 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", @@ -4264,8 +4359,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; @@ -4281,8 +4376,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) }, ], @@ -4290,9 +4385,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") { @@ -4341,7 +4436,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; @@ -4381,7 +4476,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; @@ -4450,7 +4545,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); } @@ -4466,9 +4561,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); @@ -4476,7 +4571,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); @@ -4556,11 +4651,26 @@ 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); + if ( + name === "toExponential" && + call.arguments.nodes.length === 1 && + (operand.type.kind === "dyn" || + (operand.type.kind === "union" && lowerer.dynConvertible(operand.type))) + ) { + const dispatched = lowerDynPrimitiveMethodCall( + lowerer, + call, + name, + `${recv.getText()}.${name}`, + lowerer.coerceToExpected(operand, DYN), + ); + if (dispatched) return dispatched; + } if ( operand.type.kind === "dyn" || (operand.type.kind === "union" && lowerer.dynConvertible(operand.type)) @@ -4570,14 +4680,40 @@ 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 ( + name === "toPrecision" && + call.arguments.nodes.length <= 1 && + !call.arguments.nodes.some(ts.isSpreadElement) && + isJsSourceFile(call.getSourceFile()) && + recvKind === "union" + ) { + const operand = lowerer.lowerExpr(recv); + if (operand.type.kind === "union" && lowerer.dynConvertible(operand.type)) { + const dispatched = lowerDynPrimitiveMethodCall( + lowerer, + call, + name, + `${recv.getText()}.${name}`, + lowerer.coerceToExpected(operand, DYN), + ); + if (dispatched) return dispatched; + } + } + if ( + recvKind === "f64" && + (name === "toPrecision" || (name === "toExponential" && call.arguments.nodes.length === 1)) + ) { + const typed = lowerExtraNumberMethodCall(lowerer, call, name, lowerer.lowerExpr(recv)); + if (typed) return typed; + } 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; @@ -4586,16 +4722,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 @@ -4634,7 +4770,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, @@ -4642,7 +4778,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, @@ -4650,7 +4786,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) || @@ -4672,16 +4808,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") { @@ -4729,7 +4865,7 @@ function implicitReceiverOverloadReturn( if ( !ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression) || - expr.arguments.some(ts.isSpreadElement) + expr.arguments.nodes.some(ts.isSpreadElement) ) { return null; } @@ -4742,16 +4878,16 @@ function implicitReceiverOverloadReturn( if (!member) return null; const signatures = lowerer.checker.getCallSignatures(lowerer.checker.getTypeOfSymbol(member)); const matches = signatures.filter((signature) => { - const decl = lowerer.checker.signatureDeclaration(signature); + const decl = signature.declaration; 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]!)); @@ -4783,7 +4919,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, ); } @@ -4794,7 +4930,7 @@ function reconcileOverloadReturn( call: IrExpr, ): IrExpr { const rsig = lowerer.checker.getResolvedSignature(expr); - const rdecl = rsig ? lowerer.checker.signatureDeclaration(rsig) : undefined; + const rdecl = rsig ? rsig.declaration : undefined; // Nominal covariant overrides share the base's return ABI, but retain // their declared subclass result at the call site. Unannotated fluent // JS overrides use the same bridge for their proven receiver result. @@ -4897,7 +5033,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( @@ -4916,7 +5052,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)) { @@ -5159,7 +5295,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); } @@ -5202,15 +5338,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" @@ -5285,9 +5421,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)); @@ -5534,8 +5670,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 @@ -5577,7 +5714,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)`, ); }; @@ -5615,7 +5752,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, @@ -5652,7 +5789,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)", ); } @@ -5664,7 +5801,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)}')`, ); } @@ -5760,7 +5897,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", @@ -5776,7 +5913,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 () => { ... })", ); } @@ -5870,7 +6007,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( @@ -5921,7 +6058,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", @@ -5970,23 +6107,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); } @@ -6044,7 +6181,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 { @@ -6136,9 +6273,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 = @@ -6371,21 +6508,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", ); } @@ -6413,12 +6550,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", @@ -6439,11 +6576,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 }; } @@ -6464,12 +6601,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; @@ -6492,6 +6629,7 @@ export function recordKeysArrayCall( loc: SrcLoc, ): IrExpr { const resultT = arrayOf(STRING); + lowerer.keyOrder.observe(argIr); const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -6712,12 +6850,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) }; } @@ -6753,7 +6891,7 @@ function lowerIntlNumberFormatCall( ) { return null; } - ctorArgs = recv.arguments ?? []; + ctorArgs = recv.arguments?.nodes ?? []; } else { return null; } @@ -6785,13 +6923,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))}'`, @@ -6840,10 +6978,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" || @@ -6867,12 +7005,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, @@ -6889,7 +7027,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") { @@ -6911,7 +7049,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", @@ -6922,8 +7060,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 && @@ -6963,16 +7101,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; @@ -7098,18 +7239,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 @@ -7142,9 +7283,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", @@ -7163,10 +7304,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)) || @@ -7180,7 +7321,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, }, @@ -7197,14 +7340,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); @@ -7224,17 +7370,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 }; @@ -7258,8 +7407,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, @@ -7291,15 +7440,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); @@ -7319,7 +7470,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++) { @@ -7388,12 +7539,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 = @@ -7417,7 +7568,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" && @@ -7428,12 +7579,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 } : {}), }; } @@ -7444,8 +7595,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 { @@ -7456,7 +7607,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", @@ -7475,12 +7626,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; @@ -7498,20 +7649,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", @@ -7526,10 +7677,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" || @@ -7573,15 +7724,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; @@ -7636,11 +7787,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") { @@ -7698,8 +7849,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 @@ -7720,7 +7872,7 @@ function lowerObjectStaticCall( ? lowerer.checker.getSymbolAtLocation(argNode) : undefined; const namespace = symbol - ? lowerer.checker.declarationsOf(symbol).some((decl) => ts.isNamespaceImport(decl)) + ? symbol.declarations.some((decl) => ts.isNamespaceImport(decl)) : false; const names = namespace ? [...UTIL_TYPES_KEYS, "default"].sort() : UTIL_TYPES_KEYS; const loc = locOf(call); @@ -7958,6 +8110,7 @@ function lowerObjectStaticCall( valueT = tupleShape.fields.find((f) => f.name === "1")!.type; } + lowerer.keyOrder.observe(argIr); const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { @@ -8280,11 +8433,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; @@ -8295,7 +8449,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)); @@ -8362,7 +8516,7 @@ function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExp function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType { const { checker } = lowerer; const signature = checker.getResolvedSignature(call); - const decl = signature ? checker.signatureDeclaration(signature) : undefined; + const decl = signature ? signature.declaration : undefined; const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined; const source = declared && declared.getTypeParameters().length === 0 ? declared : signature; const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null; @@ -8441,7 +8595,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") @@ -8455,7 +8609,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, }; @@ -8526,7 +8680,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", @@ -8554,7 +8708,7 @@ const SP_BRAND_METHODS = new Set([ function arrowSignatureOf(lowerer: Lowerer, node: ts.Expression): ts.Signature | null { const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node)); if (signatures.length !== 1) return null; - const declaration = lowerer.checker.signatureDeclaration(signatures[0]!); + const declaration = signatures[0]!.declaration; return declaration !== undefined && ts.isArrowFunction(declaration) ? signatures[0]! : null; } @@ -8569,8 +8723,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; @@ -8589,8 +8743,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}`; @@ -8683,7 +8837,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, @@ -8704,8 +8858,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; @@ -8723,14 +8877,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 => { @@ -8741,7 +8895,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", @@ -8781,7 +8935,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 }; @@ -8807,7 +8961,7 @@ function lowerObjLitGenericMethodCall( const recvT = lowerer.typeOf(access.expression); const propSym = lowerer.checker.getPropertyOfType(recvT, name); if (!propSym) return null; - const propertyDecl = lowerer.checker.valueDeclarationOf(propSym); + const propertyDecl = propSym.valueDeclaration; if ( propertyDecl && ts.isShorthandPropertyAssignment(propertyDecl) && @@ -8831,13 +8985,11 @@ function lowerObjLitGenericMethodCall( // claimed compilable ones; a class that failed collection keeps its // own diagnostics and the generic method-call fence downstream). if ( - lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { return null; } @@ -8875,7 +9027,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 { @@ -8883,7 +9035,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, }; @@ -8904,14 +9056,14 @@ function lowerObjLitGenericMethodCall( // receiver's runtime class must be provable — name that discipline // instead of the object-literal wording when the method lives on an // interface. - const onInterface = lowerer.checker - .declarationsOf(propSym) - .some((d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent)); + const onInterface = propSym.declarations.some( + (d) => d.parent !== undefined && ts.isInterfaceDeclaration(d.parent), + ); // A receiver held as a checked-dynamic value (a class-backed interface) // 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); @@ -8920,7 +9072,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, }; @@ -8944,7 +9096,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 }; } @@ -8960,7 +9112,17 @@ export function lowerObjectMethodCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; - let mappedReceiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); + let mappedReceiver = + structuralReadType(lowerer, access.expression) ?? + 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. @@ -9155,14 +9317,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, @@ -9177,10 +9339,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, @@ -9212,7 +9374,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") @@ -9225,7 +9387,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, }; @@ -9246,7 +9408,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}`); @@ -9270,7 +9432,7 @@ export function lowerObjectMethodCall( lowerer, value, method, - call.arguments, + call.arguments.nodes, fallback, loc, access.getText(), @@ -9302,7 +9464,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", @@ -9363,7 +9525,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") @@ -9376,7 +9538,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, }; @@ -9407,7 +9569,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, @@ -9457,7 +9619,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); @@ -9529,7 +9691,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 }], @@ -9566,6 +9728,44 @@ function unionObjectMethodHelper( if (existing) return existing; const name = `%union.call.${lowerer.unionCallHelpers.size}`; lowerer.unionCallHelpers.set(key, name); + // A probe that catches a refusal while the body lowers must not leave + // the name registered for a helper that was never emitted. + try { + lowerer.liftedFns.push( + unionObjectMethodFunction( + lowerer, + name, + sourceCall, + receiverT, + method, + plans, + shapes, + resultT, + loc, + ), + ); + } catch (e) { + lowerer.unionCallHelpers.delete(key); + throw e; + } + return name; +} + +function unionObjectMethodFunction( + lowerer: Lowerer, + name: string, + sourceCall: ts.CallExpression, + receiverT: IrType & { kind: "union" }, + method: string, + plans: { + arm: IrType & { kind: "object" }; + info: ClassInfo; + found: NonNullable>; + }[], + shapes: readonly ParamShape[], + resultT: IrType, + loc: SrcLoc, +): IrFunction { const receiver = varRef("this.0", receiverT, loc); const argRefs = shapes.map((shape, i) => varRef(`a.${i}`, shape.type, loc)); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -9640,8 +9840,7 @@ function unionObjectMethodHelper( type: param.type, mutable: false, })); - lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc }); - return name; + return { name, params, returnType: resultT, locals, body, loc }; } /** The argument's lowered type for a message (what the static form saw), @@ -9834,6 +10033,22 @@ export function lowerStaticNumberGlobal( return null; } +/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ +function lowerWebCryptoGetRandomValues( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if ( + access.name.text !== "getRandomValues" || + call.questionDotToken || + access.questionDotToken || + !isWebCryptoReceiver(lowerer, access.expression) + ) + return null; + return lowerCryptoGetRandomValues(lowerer, call); +} + /** A then handler's promise-armed result (`.then(() => cached ?? load())` * typed `T | Promise`, or a `Promise | void` callback's result): * the lifted async body adopts the promise arm and fulfills with the diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index d44748084..9bae6f682 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 => { @@ -174,6 +178,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { return valid; } +/** returnsOnlyThis, also accepting a comma expression whose last operand + * is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent + * claim setters). Used only to narrow a call's result back to the + * receiver's class. */ +export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { + const body = member.body; + const last = body?.statements?.nodes.at(-1); + if (!body || !last || !ts.isReturnStatement(last)) return false; + let valid = true; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node)) { + let value = node.expression; + for (;;) { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if ( + value && + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + value = value.right; + continue; + } + break; + } + if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; + } + node.forEachChild(visit); + }; + body.forEachChild(visit); + return valid; +} + function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; @@ -217,14 +254,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { const value = storedClassValueType(lowerer, declaration.initializer, visiting); if (value?.kind === "classval") return value; @@ -239,7 +274,7 @@ export function storedClassValueType( const mapped = lowerer.mapTypeOf(type); if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) { for (const signature of lowerer.checker.getConstructSignatures(type)) { - const constructor = lowerer.checker.signatureDeclaration(signature); + const constructor = signature.declaration; const declaration = constructor && ts.isConstructorDeclaration(constructor) ? constructor.parent : undefined; if ( @@ -280,14 +315,12 @@ export function storedClassValueType( const symbol = lowerer.resolveValueSymbol(callee); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find( - (decl) => - ts.isVariableDeclaration(decl) && - decl.initializer && - (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, - ); + symbol.declarations.find( + (decl) => + ts.isVariableDeclaration(decl) && + decl.initializer && + (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0, + ); if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { callee = declaration.initializer; continue; @@ -298,7 +331,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); @@ -349,7 +382,7 @@ export function storedClassValueType( // Cache the module-level closure so its captures and lifted bodies are // exactly the ones used when the declaration executes. for (const signature of lowerer.checker.getConstructSignatures(type)) { - const declaration = lowerer.checker.signatureDeclaration(signature); + const declaration = signature.declaration; const cls = declaration && ts.isConstructorDeclaration(declaration) ? declaration.parent : null; if (!cls || (!ts.isClassExpression(cls) && !ts.isClassDeclaration(cls))) continue; let factory: ts.ArrowFunction | ts.FunctionExpression | undefined; @@ -367,7 +400,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) || @@ -539,6 +572,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; @@ -716,7 +754,7 @@ export interface GenericClassInfo { * or the ElementAccessExpression itself). Null when no declaration has * that shape (well-known-symbol members like `[Symbol.iterator]`). */ function lateBoundKeySymOf(lowerer: Lowerer, p: ts.Symbol): ts.Symbol | null { - for (const d of lowerer.checker.declarationsOf(p)) { + for (const d of p.declarations) { const access = ts.isBinaryExpression(d) && ts.isElementAccessExpression(d.left) ? d.left @@ -755,8 +793,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[] = []; @@ -782,6 +820,17 @@ function symbolSlotReturnType( return out; } +/** The error classes whose values construct through error.newOptions. */ +const ERROR_CLASS_VALUES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + /** The builtin Error hierarchy * as eagerly-registered ClassInfos: mapType names them the moment a lib * Error type appears, so the infos must exist before any lowering. They @@ -860,6 +909,17 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void { { type: DYN, mode: "omittable" }, { type: DYN, mode: "omittable" }, ], + // The ECMAScript error constructors take (message?, options?); + // DOMException, AggregateError, AssertionError and SuppressedError + // have their own construction and are not values. + ...(ERROR_CLASS_VALUES.has(irName) + ? { + valueCtorParams: [ + { type: DYN, mode: "omittable" as const }, + { type: DYN, mode: "omittable" as const }, + ], + } + : {}), base, subclasses: [], throwingSetters: [], @@ -892,6 +952,7 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void { // `new EventEmitter()` — zero-argument (the options bag fences at // construction sites; the checker may admit it via @types/node). ctorParams: [], + valueCtorParams: [], base: null, subclasses: [], throwingSetters: [], @@ -923,6 +984,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: [], @@ -955,7 +1019,7 @@ export function builtinStreamInfoOf( // member to the stdlib class symbol. The declaration itself is // alias plumbing (streamClassAliasDecl — both declaration walks // skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -977,7 +1041,7 @@ export function builtinStreamInfoOf( if (rec.lib === symbol.name) irName = name; } if (!irName) return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d)) return false; if (isNodeTypesPath(d.getSourceFile().fileName)) return false; let node: ts.Node | undefined = d.parent; @@ -999,15 +1063,13 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(expression); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ((ts.isConstructorDeclaration(decl.parent) && - !checkedJsDocumentedParameter(lowerer, decl)) || - (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ((ts.isConstructorDeclaration(decl.parent) && + !checkedJsDocumentedParameter(lowerer, decl)) || + (lowerer.typeOf(expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0), + ) ) return DYN; } @@ -1057,7 +1119,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) || @@ -1067,7 +1129,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E const symbol = lowerer.checker.getSymbolAtLocation(part.name); if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { const member = ts.isBinaryExpression(decl) ? decl.left : decl; return ( ts.isPropertyAccessExpression(member) && @@ -1084,7 +1146,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) @@ -1209,13 +1271,10 @@ function returnsWidenedJsField( : undefined; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - ts.isMethodDeclaration(declaration) && - lowerer.checkedVirtualJsMethods.has(declaration), - ) + symbol.declarations.some( + (declaration) => + ts.isMethodDeclaration(declaration) && lowerer.checkedVirtualJsMethods.has(declaration), + ) ) return true; } @@ -1228,14 +1287,12 @@ function returnsWidenedJsField( const symbol = lowerer.checker.getSymbolAtLocation(expression.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isGetAccessorDeclaration(declaration) || - ts.isSetAccessorDeclaration(declaration)) && - lowerer.checkedVirtualJsAccessors.has(declaration), - ) + symbol.declarations.some( + (declaration) => + (ts.isGetAccessorDeclaration(declaration) || + ts.isSetAccessorDeclaration(declaration)) && + lowerer.checkedVirtualJsAccessors.has(declaration), + ) ) return true; } @@ -1268,11 +1325,11 @@ function returnsWidenedJsField( * (genResultRecord's list, including scalar-backed Date values). */ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null { const t = lowerer.checker.getTypeOfSymbol(p); - const site = lowerer.checker.declarationsOf(p)[0]; + const site = p.declarations[0]; let mapped = lowerer.mapTypeOf(t) ?? (site ? dynFallbackType(lowerer, site, t) : null); if (mapped && site) mapped = jsOpenObjectType(site, mapped, lowerer.shapes, lowerer.unions); if (!mapped || mapped.kind === "void") return null; - for (const declaration of lowerer.checker.declarationsOf(p)) { + for (const declaration of p.declarations) { const assignment = ts.isBinaryExpression(declaration) ? declaration : declaration.parent; if ( assignment && @@ -1319,7 +1376,7 @@ export function builtinEmitterInfoOf( // follow the member off the module namespace. The declaration // itself is alias plumbing (builtinMemberRequireDecl — both // declaration walks skip it). - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1335,7 +1392,7 @@ export function builtinEmitterInfoOf( return null; } if (symbol.name !== "EventEmitter") return null; - const declared = lowerer.checker.declarationsOf(symbol).some((d) => { + const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; while (node) { @@ -1393,9 +1450,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 @@ -1453,13 +1510,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; @@ -1496,15 +1553,15 @@ 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 = reference && ts.isIdentifier(reference) ? lowerer.resolveValueSymbol(reference) : undefined; const baseDeclaration = baseSymbol - ? lowerer.checker.declarationsOf(baseSymbol).find(ts.isClassDeclaration) + ? baseSymbol.declarations.find(ts.isClassDeclaration) : undefined; if ( baseDeclaration && @@ -1528,10 +1585,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 @@ -1545,9 +1602,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); @@ -1562,7 +1619,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; @@ -1602,10 +1659,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 && @@ -1624,9 +1681,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( @@ -1638,7 +1695,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 @@ -1710,7 +1767,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 @@ -1776,7 +1833,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"; @@ -1851,8 +1908,7 @@ export function collectClassShapeInner( return true; } const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(expression)); - const source = - signatures.length === 1 ? lowerer.checker.signatureDeclaration(signatures[0]!) : undefined; + const source = signatures.length === 1 ? signatures[0]!.declaration : undefined; if ( !source || !isJsSourceFile(source.getSourceFile()) || @@ -1877,13 +1933,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 @@ -1918,14 +1974,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; } @@ -1948,7 +2004,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; } @@ -1966,7 +2022,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 && @@ -1987,12 +2044,9 @@ export function collectClassShapeInner( // what actually blocks it). const rebinds = memberSym !== undefined && - lowerer.checker - .declarationsOf(memberSym) - .filter( - (d) => - ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, - ).length > 1; + memberSym.declarations.filter( + (d) => ts.isBinaryExpression(d) && d.operatorToken.kind === ts.SyntaxKind.EqualsToken, + ).length > 1; if (rebinds) { lowerer.unsupported( "SC1090", @@ -2007,7 +2061,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; @@ -2018,7 +2072,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; @@ -2131,7 +2185,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; @@ -2144,9 +2198,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"); @@ -2221,13 +2275,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) && @@ -2266,12 +2322,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) && @@ -2288,7 +2344,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()) && @@ -2456,7 +2512,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 @@ -2551,12 +2607,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; @@ -2815,11 +2871,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) || @@ -2973,8 +3029,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 || @@ -2985,7 +3041,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 @@ -3265,7 +3321,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 && @@ -3329,16 +3385,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)); @@ -3372,7 +3428,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 @@ -3497,7 +3553,7 @@ export function collectClassShapeInner( sig = { params: [], ret }; } else { // tsc rejects optional/default/rest setter params (TS1051-53). - sig = { params: lowerer.paramShapes(member.parameters), ret: VOID }; + sig = { params: lowerer.paramShapes(member.parameters.nodes), ret: VOID }; } if (lowerer.checkedVirtualJsAccessors.has(member)) { if (isGet) sig.ret = DYN; @@ -3569,7 +3625,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. @@ -3598,7 +3654,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 && @@ -3630,7 +3686,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) && @@ -3817,7 +3873,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) && @@ -3977,14 +4033,12 @@ export function collectClassShapeInner( if (fields.has(p.name) || methods.has(p.name)) continue; if (hasAccessor(p.name)) continue; if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; - const site = lowerer.checker - .declarationsOf(p) - .find( - (d) => - ts.isPropertyAccessExpression(d) || - ts.isBinaryExpression(d) || - ts.isElementAccessExpression(d), - ); + const site = p.declarations.find( + (d) => + ts.isPropertyAccessExpression(d) || + ts.isBinaryExpression(d) || + ts.isElementAccessExpression(d), + ); if (!site) continue; // Late-bound properties: the ones the scan above collected are // real fields under their printable names — skip. The rest keep @@ -4055,10 +4109,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)", ); } @@ -4067,7 +4121,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 && @@ -4091,13 +4145,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, ), @@ -4181,7 +4235,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); @@ -4688,8 +4742,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void : null; if (ambientCallee !== null) { const sym = lowerer.resolveValueSymbol(ambientCallee); - const vdecl = - sym && !lowerer.isStdlibSymbol(sym) ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const vdecl = sym && !lowerer.isStdlibSymbol(sym) ? sym.declarations[0] : undefined; const ambientVar = vdecl !== undefined && ts.isVariableDeclaration(vdecl) && @@ -4950,7 +5003,7 @@ function constructorMethodReads( const out: ts.Node[] = []; let owner: ts.ClassLikeDeclaration | undefined = decl; for (let next = base; owner; owner = next?.decl ?? undefined, next = next?.base ?? null) { - for (const member of owner.members) { + for (const member of owner.members.nodes) { if ( (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && @@ -5059,11 +5112,20 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node) ); } if (info.builtinError || info.builtinEmitter || info.builtinStream !== undefined) { - lowerer.unsupported( - "SC1090", - blame, - `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, - ); + if (info.valueCtorParams === undefined) { + lowerer.unsupported( + "SC1090", + blame, + `builtin classes as values ('${display}' is runtime-provided — reference program-declared classes instead)`, + ); + } + defineClassValueConstruct(lowerer, info); + return { + kind: "classRef", + className: info.def.name, + type: { kind: "classval", className: info.def.name }, + loc: locOf(blame), + }; } if ( lowerer.inheritsBuiltinEmitterCtor(info) || @@ -5090,14 +5152,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)) @@ -5302,14 +5442,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()) && @@ -5317,7 +5460,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( @@ -5333,9 +5476,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) || @@ -5367,9 +5510,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; } } @@ -5404,7 +5547,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) && @@ -5434,7 +5577,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); @@ -5619,7 +5762,7 @@ export function exactClassOfReceiver(lowerer: Lowerer, expr: ts.Expression): Cla // static write would create an own property in JS. The general // class-value write fence answers instead. if (direct) return direct.classDecorators?.valueGlobalId !== undefined ? null : direct; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -5667,7 +5810,7 @@ export function propertyAssignedClassInfoOf( symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; const registered = lowerer.classBySymbol.get(resolved); if (registered) return registered; - const decls = lowerer.checker.declarationsOf(resolved); + const decls = resolved.declarations; // The class expression's OWN symbol (tsgo's answer through CJS export // aliases): the declaration is the expression itself. Its top-level // assignment statement must be the binding's ONLY producer. The @@ -5732,7 +5875,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 ( @@ -5875,7 +6018,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, }; @@ -5922,7 +6065,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)) { @@ -5939,7 +6082,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) { @@ -5953,7 +6096,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) { @@ -5991,14 +6134,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 }; } @@ -6043,15 +6186,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(); @@ -6081,7 +6224,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), }; @@ -6131,7 +6274,7 @@ function capturesEnclosingScope( if (captured) return; if (ts.isIdentifier(node)) { const symbol = lowerer.checker.getSymbolAtLocation(node); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; // The enclosing function's own name is the same binding for every // evaluation. if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true; @@ -6232,6 +6375,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) || @@ -6331,11 +6477,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), }; } @@ -6470,7 +6616,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(); @@ -6503,8 +6649,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) @@ -6532,7 +6680,7 @@ export function collectInheritedJsFieldWrites( for (const base of lowerer.checker.getBaseTypes(target)) { for (const [name, expressions] of writes) { const property = lowerer.checker.getPropertyOfType(base, name); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if (ts.isMethodDeclaration(declaration) || ts.isAccessor(declaration)) continue; let owner: ts.Node | undefined = declaration.parent; while (owner && !ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) @@ -6567,14 +6715,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()) ) { @@ -6589,7 +6739,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of accessors) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( (ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration)) && @@ -6597,7 +6747,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) && @@ -6612,13 +6762,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(); @@ -6630,7 +6782,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const member of members) { if (!ts.isIdentifier(member.name)) continue; const property = lowerer.checker.getPropertyOfType(base, member.name.text); - for (const declaration of property ? lowerer.checker.declarationsOf(property) : []) { + for (const declaration of property ? property.declarations : []) { if ( ts.isMethodDeclaration(declaration) && isJsSourceFile(declaration.getSourceFile()) && @@ -6639,25 +6791,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) { @@ -6715,7 +6869,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), ); } @@ -6737,7 +6892,7 @@ export function exactInstanceClassOf(lowerer: Lowerer, expr: ts.Expression): Cla if (direct) return direct; if (!ts.isIdentifier(e)) return null; const symbol = lowerer.resolveValueSymbol(e); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -6847,20 +7002,44 @@ export function lowerClassGenericMethodCall( loc, } : upcastTo(lowerer, receiver, declarer.def.name); - const args = lowerer.completeArgs(call.arguments, instance.params, loc, call); - return { + const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); + const result: IrExpr = { kind: "call", callee: instance.name, args: [thisArg, ...args], type: instance.returnType, loc, }; + // A fluent JavaScript method (every return ends in `this`, jose's claim + // setters) answers the receiver: the instance returns the declarer's + // class, a used result's checker type is the receiver's subclass. + if ( + result.type.kind === "object" && + !ts.isExpressionStatement(call.parent) && + ts.isMethodDeclaration(info.decl) && + isJsSourceFile(info.decl.getSourceFile()) && + returnsOnlyThisValue(info.decl) + ) { + return lowerer.maybeNarrow(result, call); + } + return result; } /** Wraps a derived-class expression in an upcast when the target base * class differs (a no-op reinterpret at runtime; keeps IR types exact). */ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr { if (expr.type.kind === "object" && expr.type.className !== className) { + // A value lowered at an ANCESTOR of the class the site needs (a + // fluent `this` result whose lowering kept the method's declaring + // class, the receiver of the next call in a chain): the checker + // vouches for the subclass, so the bridge is the verified downcast, + // never an upcast the validator rejects (an internal error before). + if ( + !lowerer.isSubclassOf(expr.type.className, className) && + lowerer.isSubclassOf(className, expr.type.className) + ) { + return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; + } return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; } return expr; @@ -6879,19 +7058,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; @@ -7048,7 +7227,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); @@ -7063,7 +7242,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)); } @@ -7116,7 +7295,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) && @@ -7560,7 +7739,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. @@ -7570,7 +7749,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}`, @@ -7605,7 +7784,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)); @@ -7632,13 +7811,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}`, @@ -7883,7 +8062,7 @@ export function lowerDerivedCtorBody( }, ]; if (info.callableBase) { - if (superCall.arguments.some(ts.isSpreadElement)) + if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, @@ -7891,14 +8070,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, @@ -7910,7 +8089,7 @@ export function lowerDerivedCtorBody( lowerer, info, base, - superCall.arguments, + superCall.arguments.nodes, thisLocal, loc, superCall, @@ -7922,10 +8101,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, @@ -7937,20 +8116,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, @@ -7961,7 +8140,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), @@ -8005,7 +8184,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: { @@ -8207,7 +8386,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, @@ -8270,7 +8449,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, }; @@ -8287,7 +8466,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}`, @@ -8550,25 +8729,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 { @@ -8627,7 +8806,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { let intlConstructor = expr.expression; if (ts.isIdentifier(intlConstructor)) { const symbol = lowerer.resolveValueSymbol(intlConstructor); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) intlConstructor = declaration.initializer; } @@ -8651,11 +8830,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const symbol = lowerer.resolveValueSymbol(expr.expression); const declaration = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((node) => ts.isFunctionDeclaration(node) && node.body !== undefined); + symbol.declarations.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) && @@ -8702,7 +8879,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]))) { @@ -8718,7 +8895,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)) && @@ -8769,10 +8946,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')", ); } @@ -8794,7 +8971,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 @@ -8880,7 +9057,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, @@ -8921,7 +9103,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 @@ -8936,7 +9118,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); } @@ -8975,7 +9157,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); } @@ -8988,7 +9170,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); } @@ -9003,7 +9185,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); } @@ -9017,7 +9199,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", @@ -9031,10 +9213,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')", ); } @@ -9055,7 +9237,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). @@ -9065,7 +9247,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); } @@ -9145,7 +9327,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`, @@ -9199,7 +9381,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); } @@ -9272,22 +9454,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); @@ -9304,8 +9486,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 @@ -9324,7 +9506,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, @@ -9335,11 +9517,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 }; } @@ -9354,7 +9536,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) ) { @@ -9392,7 +9574,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", @@ -9416,22 +9598,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, @@ -9446,12 +9628,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 && @@ -9463,7 +9647,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, @@ -9487,7 +9671,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, @@ -9500,7 +9684,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"); } @@ -9512,7 +9696,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const a0 = args[0]!; if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the @@ -9572,12 +9756,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" || @@ -9589,7 +9773,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", @@ -9653,7 +9837,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", @@ -9777,24 +9961,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); @@ -9860,7 +10076,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..41c6704c5 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"); } @@ -162,7 +162,7 @@ export function rejectComptimeCaptures( const symbol = ts.isShorthandPropertyAssignment(parent) ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) : lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : undefined; + const decls = symbol ? symbol.declarations : undefined; if (symbol && decls && decls.length > 0) { const ownLocal = decls.every( (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end, 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..159d72f32 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -73,7 +73,7 @@ export function lowerEnumAccess( if (ts.isPropertyAccessExpression(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl) && effectFreeReceiver(expr.expression)) { return lowerEnumMemberRead(lowerer, expr, decl); } @@ -86,9 +86,9 @@ export function lowerEnumAccess( if (!ts.isIdentifier(recv)) return null; const recvSym = lowerer.resolveValueSymbol(recv); if (!recvSym || !(recvSym.flags & ts.SymbolFlags.Enum)) return null; - const decls = lowerer.checker - .declarationsOf(recvSym) - .filter((d): d is ts.EnumDeclaration => ts.isEnumDeclaration(d)); + const decls = recvSym.declarations.filter((d): d is ts.EnumDeclaration => + ts.isEnumDeclaration(d), + ); if (decls.length === 0) return null; let key = expr.argumentExpression; while (ts.isParenthesizedExpression(key)) key = key.expression; @@ -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); } @@ -222,7 +222,7 @@ function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null { if (ts.isPropertyAccessExpression(x)) { const sym = lowerer.checker.getSymbolAtLocation(x.name); if (sym && sym.flags & ts.SymbolFlags.EnumMember) { - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (decl && ts.isEnumMember(decl)) { const v = lowerer.checker.getConstantValue(decl); if (typeof v === "number") return v; @@ -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..06e9c4ab1 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")) @@ -419,7 +420,7 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { // binding's type in JavaScript; its source property still names it. if (!ts.isIdentifier(arg)) return false; const symbol = lowerer.resolveValueSymbol(arg); - const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if (!declaration || !ts.isBindingElement(declaration)) return false; const pattern = declaration.parent; const variable = pattern?.parent; @@ -474,7 +475,7 @@ function staticStringPrefixOf( const symbol = lowerer.resolveValueSymbol(expression); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -503,12 +504,12 @@ 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; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( declaration && ts.isVariableDeclaration(declaration) && @@ -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..5ed97d4d7 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -97,7 +97,7 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n // existing member paths stay put. const sym = lowerer.resolveValueSymbol(recv); if (!sym) return null; - const decl = lowerer.checker.valueDeclarationOf(sym); + const decl = sym.valueDeclaration; if (!decl || decl.getSourceFile().isDeclarationFile) return null; if (isJsSourceFile(decl.getSourceFile())) return null; if (ts.isFunctionDeclaration(decl)) { @@ -202,7 +202,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void const memberName = typeof w.key === "string" ? w.key - : `sym%${w.key.name}%${lowerer.checker.declarationsOf(w.key)[0]?.getStart() ?? 0}`; + : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`; const g: IrGlobal = { id: `%g.${tag}${fnName}%.${memberName}`, name: `${fnName}.${memberName}`, @@ -239,7 +239,7 @@ function nsOwnedMember( const nameNode = ts.isPropertyAccessExpression(access) ? access.name : access.argumentExpression; const sym = lowerer.checker.getSymbolAtLocation(nameNode); if (!sym) return false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { for ( let p: ts.Node | undefined = d.parent; p !== undefined && !ts.isSourceFile(p); @@ -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 32f80ef5b..bbf1d6087 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6,13 +6,14 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js"; import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; -import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; +import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; @@ -39,6 +40,8 @@ import { lowerZlibStreamBytesWritten, } from "./builtins/compression.js"; import { + runtimeClassValueOf, + runtimeClassValueOfType, builtinClassAliasOwnsNoSlot, builtinClassRefOf, sameBuiltinClass, @@ -66,7 +69,12 @@ import { } from "./maybe-promise.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; -import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; +import { + narrowClassUnion, + narrowStoredClassValue, + structuralNarrowing, + structuralReadType, +} from "./class-unions.js"; import { lowerUnionFieldRead, lowerUnionFieldWrite } from "./expressions/union-fields.js"; import { captureContextArguments, declareContextLocal } from "./function-context.js"; import { OBJECT_CALLABLE_VALUES } from "./surfaces.js"; @@ -146,6 +154,7 @@ import { unsupportedDiag, } from "../../diagnostics/diagnostic.js"; import { PoisonError, dynFallbackType, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js"; +import { assertsGlobalObject, extractionCopiesReference } from "./assertion-identity.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerHelperReturnCall } from "./containers/iterator-helpers.js"; import { @@ -213,6 +222,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js"; import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { @@ -343,23 +353,41 @@ export function abstractPropertyDeclOf( ): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker - .declarationsOf(sym) - .some( - (d) => - ts.isPropertyDeclaration(d) && - ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, - ); + return sym.declarations.some( + (d) => + ts.isPropertyDeclaration(d) && + ts.getModifiers(d)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword) === true, + ); } /** A field declared by node:util's parseArgs checked-dynamic type family. * Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias * identity no longer reaches mapType; declaration ancestry is the stable * provenance check. */ +/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */ +const UINT8ARRAY_BASE64_METHODS: ReadonlySet = new Set([ + "toBase64", + "toHex", + "setFromBase64", + "setFromHex", +]); + +/** `Uint8Array.prototype.` read off the global. */ +function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false; + const proto = expr.expression; + return ( + ts.isPropertyAccessExpression(proto) && + !proto.questionDotToken && + proto.name.text === "prototype" && + stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array" + ); +} + function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; - return lowerer.checker.declarationsOf(sym).some((d) => { + return sym.declarations.some((d) => { if (!lowerer.isStdlibFile(d.getSourceFile())) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { @@ -387,7 +415,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression const symbol = lowerer.checker.getSymbolAtLocation(expr.name); return ( !!symbol && - lowerer.checker.declarationsOf(symbol).some((declaration) => { + symbol.declarations.some((declaration) => { if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false; const parent = declaration.parent; if (!ts.isInterfaceDeclaration(parent)) return false; @@ -537,7 +565,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), @@ -644,6 +672,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return { kind: "strLit", value: "object", type: STRING, loc }; if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + if (isNavigatorGlobal(lowerer, expr.expression)) + return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); if (performance) return performance; if (isNodeModuleValue(lowerer, expr.expression)) { @@ -706,7 +736,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { lowerer.isStdlibMember(tested) ) { const sym = lowerer.checker.getSymbolAtLocation(tested.name); - const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; if ( decls.some(isBuiltinModuleValueDecl) && decls.every((decl) => builtinModuleExportParent(decl) !== undefined) @@ -720,6 +750,31 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { } } } + // `typeof globalThis.AbortSignal` / `typeof Headers`: Node 24's Web + // platform constructor globals are functions. JavaScript sources + // represent such a value as an identity token, and the fallback + // declarations type some (AbortSignal) without a construct signature, + // so neither the operand nor the type can answer; the read has no + // effects. + if (constructorGlobalTypeof(lowerer, tested) !== null) + return { kind: "strLit", value: "function", type: STRING, loc }; + // `typeof globalThis.AbortSignal.any` (ky's feature probe): a static + // member of a standard-library global, declared callable, whose value + // has no representation. The read has no effects and Node's answer + // for every such function is "function"; members that lower as values + // keep their own answer. + if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) { + const owner = stdlibGlobalNameOf(lowerer, tested.expression); + if (owner !== null && lowerer.isStdlibMember(tested)) { + const t = lowerer.typeOf(tested); + if ( + (lowerer.checker.getCallSignatures(t).length > 0 || + lowerer.checker.getConstructSignatures(t).length > 0) && + tryLowerExpression(lowerer, tested) === null + ) + return { kind: "strLit", value: "function", type: STRING, loc }; + } + } if (ts.isIdentifier(tested)) { const sym = lowerer.checker.getSymbolAtLocation(tested); if (!sym) { @@ -940,6 +995,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)) { @@ -963,7 +1025,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { recordTextCodecClass(lowerer.shapes.get(local.type.shapeId)!) !== null ) { const symbol = lowerer.resolveValueSymbol(expr); - const decl = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + const decl = symbol ? symbol.valueDeclaration : undefined; if ( decl && ts.isVariableDeclaration(decl) && @@ -1306,6 +1368,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (bi.module === "os" && bi.member === "constants") { return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc }; } + if (bi.module === "fs" && bi.member === "constants") + return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc }; // `import { O_RDONLY } from "constants"`: the member of the frozen // module object. if (bi.module === "constants") { @@ -1545,7 +1609,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // is above); only user-file declares do. { const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -1657,6 +1721,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } + if (isNavigatorGlobal(lowerer, expr)) { + refuseNavigator( + lowerer, + expr, + "the navigator object as a value", + lowerer.checker.getSymbolAtLocation(expr), + ); + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. @@ -1920,6 +1992,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + if (!expr.questionDotToken) { + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; + } // `require("./x").member` over a program module's export table: the // module's %init at this position, then the member read exactly like // a `const m = require("./x")` binding's (cjs-inline-require.ts). @@ -1962,6 +2038,16 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ref = builtinClassRefOf(lowerer, expr.expression); if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc }; } + // `C.name` through a runtime-provided class VALUE (a `typeof Readable` + // parameter): the class object's stored name. + if ( + expr.name.text === "name" && + !expr.questionDotToken && + runtimeClassValueOfType(lowerer, lowerer.typeOf(expr.expression)) !== null + ) { + const read = lowerer.lowerFieldRead(expr); + if (read) return read; + } // Expando statics may still be absent even when JS inference sees // the eventual assignment's scalar type. Preserve the checked value. const staticField = lowerStaticFieldRead(lowerer, expr); @@ -2108,6 +2194,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ) { return dynUndefinedExpr(loc); } + // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex + // feature probes libraries such as jose make: the methods are newer + // than Node 24, whose Uint8Array prototype answers undefined for them. + if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc); if (expr.questionDotToken && expr.name.text === "electron") { const processProperty = lowerer.lowerProcessProperty(expr); if (processProperty) return processProperty; @@ -2161,13 +2251,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); } } @@ -2209,7 +2297,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 && @@ -2218,7 +2306,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, }; @@ -2600,13 +2688,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { (recvSym?.name === "IteratorResult" || recvSym?.name === "IteratorYieldResult" || recvSym?.name === "IteratorReturnResult") && - lowerer.checker - .declarationsOf(recvSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ) + recvSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) { const recv = lowerer.lowerExpr(expr.expression); if (recv.type.kind === "dyn") @@ -2977,11 +3063,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) : []; @@ -3055,7 +3141,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 @@ -3300,15 +3386,13 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.checker.getSymbolAtLocation(node.name); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isPropertyDeclaration(decl) && - !decl.initializer && - !decl.type && - isJsSourceFile(decl.getSourceFile()), - ) + symbol.declarations.some( + (decl) => + ts.isPropertyDeclaration(decl) && + !decl.initializer && + !decl.type && + isJsSourceFile(decl.getSourceFile()), + ) ) { // Inference describes later writes, not the initial undefined. // A use that requires the inferred type must validate the value. @@ -3376,48 +3460,44 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const symbol = lowerer.resolveValueSymbol(node); if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - ts.isMethodDeclaration(decl.parent) && - lowerer.checkedVirtualJsMethods.has(decl.parent), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + ts.isMethodDeclaration(decl.parent) && + lowerer.checkedVirtualJsMethods.has(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent)) + symbol.declarations.some( + (decl) => ts.isParameter(decl) && ts.isConstructorDeclaration(decl.parent), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl)) + symbol.declarations.some( + (decl) => ts.isVariableDeclaration(decl) && jsBindingHasOpenWrites(lowerer, decl), + ) ) return expr; if ( symbol && - lowerer.checker - .declarationsOf(symbol) - .some( - (decl) => - ts.isParameter(decl) && - decl.initializer && - !decl.type && - !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), - ), - ) + symbol.declarations.some( + (decl) => + ts.isParameter(decl) && + decl.initializer && + !decl.type && + !/@(?:param|type)\b/.test( + decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ), + ) ) return expr; if ( symbol && - lowerer.checker.declarationsOf(symbol).some((decl) => { + symbol.declarations.some((decl) => { if (!ts.isParameter(decl) || decl.type) return false; const fn = decl.parent; if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) return false; @@ -3490,6 +3570,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx narrowed.className !== expr.type.className && lowerer.isSubclassOf(narrowed.className, expr.type.className) ) { + const reduced = structuralNarrowing(lowerer, node, expr.type.className, narrowed); + const union = reduced ? narrowClassUnion(lowerer, expr, reduced) : null; + if (union) return union; if (lowerer.narrowing.untrusted(node, narrowed)) { const checked = checkedClassAssertion(lowerer, expr, narrowed, expr.loc); if (checked) return checked; @@ -4040,7 +4123,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; @@ -4094,7 +4177,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) @@ -5838,11 +5921,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) && @@ -5938,7 +6021,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]!; @@ -5950,11 +6033,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 && @@ -5973,7 +6056,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]!; } @@ -5985,7 +6068,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 @@ -6001,12 +6084,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 && @@ -6030,7 +6113,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", @@ -6044,7 +6127,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), ) ) @@ -6054,7 +6137,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) ) { @@ -6067,7 +6150,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; @@ -6079,11 +6162,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 @@ -6091,7 +6174,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) }; @@ -6115,14 +6198,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", @@ -6150,7 +6233,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; @@ -6172,8 +6255,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) { @@ -6181,7 +6264,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, @@ -6426,7 +6509,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)); @@ -7043,6 +7126,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } + if (key.type.kind === "f64") { + const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); + if (safe) return safe; + } if (key.type.kind === "string" || key.type.kind === "f64") { const lit = ts.isStringLiteral(expr.argumentExpression) ? expr.argumentExpression.text @@ -8004,6 +8091,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr } return null; } + // `m[2]` on a JavaScript `RegExpExecArray | null` that a previous + // statement ruled non-null without narrowing it (jose's secs()). + if ( + receiverT?.kind === "union" && + isJsSourceFile(expr.getSourceFile()) && + !expr.questionDotToken && + !hasOptionalChainGuard(expr.expression) + ) { + const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "array") return null; + const value = lowerer.lowerExpr(expr.expression); + const index = lowerer.lowerExpr(expr.argumentExpression); + if (index.type.kind !== "f64") return null; + return lowerOptionalArrayIndexRead(lowerer, expr, value, index); + } if (receiverT?.kind !== "array") return null; const arr = lowerer.lowerExpr(expr.expression); if (arr.type.kind !== "array") return null; @@ -9007,6 +9110,76 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx return lowerRecordPropertyKey(lowerer, key, node); } +/** `typeof` of a Node 24 Web-platform constructor global or one of its + * static methods: "function", decided by name. The read has no effects. + * Static methods answer by name because after a `typeof + * globalThis.AbortSignal === "function" &&` guard the checker narrows a + * fallback-typed receiver to never, hiding the member's declaration. */ +function constructorGlobalTypeof(lowerer: Lowerer, tested: ts.Expression): string | null { + while (ts.isParenthesizedExpression(tested)) tested = tested.expression; + const globalName = stdlibGlobalNameOf(lowerer, tested); + if (globalName !== null && NODE_CONSTRUCTOR_GLOBALS.has(globalName)) return "function"; + if (!ts.isPropertyAccessExpression(tested) || tested.questionDotToken) return null; + const owner = stdlibGlobalNameOf(lowerer, tested.expression); + return owner !== null && + Object.hasOwn(NODE_CONSTRUCTOR_STATICS, owner) && + NODE_CONSTRUCTOR_STATICS[owner]!.includes(tested.name.text) + ? "function" + : null; +} + +/** `typeof globalThis.AbortSignal.any === "function"`: the comparison + * folds with constructorGlobalTypeof's answer. */ +function lowerConstructorGlobalTypeofTest( + lowerer: Lowerer, + expr: ts.BinaryExpression, + loc: SrcLoc, +): IrExpr | null { + const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken; + for (const [a, b] of operandPairs(expr)) { + if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue; + const answer = constructorGlobalTypeof(lowerer, a.expression); + if (answer !== null) + return { kind: "boolLit", value: (answer === b.text) !== negated, type: BOOL, loc }; + } + return null; +} + +/** Static methods of Node 24's Web-platform constructor globals. */ +const NODE_CONSTRUCTOR_STATICS: Readonly> = { + AbortSignal: ["abort", "any", "timeout"], + Response: ["error", "json", "redirect"], + URL: ["canParse", "createObjectURL", "parse", "revokeObjectURL"], +}; + +/** Node 24's Web-platform constructor globals: `typeof` answers + * "function" for each. */ +const NODE_CONSTRUCTOR_GLOBALS: ReadonlySet = new Set([ + "AbortController", + "AbortSignal", + "Blob", + "BroadcastChannel", + "CustomEvent", + "DOMException", + "Event", + "EventTarget", + "File", + "FormData", + "Headers", + "MessageChannel", + "MessageEvent", + "MessagePort", + "ReadableStream", + "Request", + "Response", + "TextDecoder", + "TextEncoder", + "TransformStream", + "URL", + "URLSearchParams", + "WritableStream", +]); + /** 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 { @@ -9021,6 +9194,10 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr if (e.type.kind === "string") return e; if (e.type.kind === "url") return { kind: "libCall", fn: "url.href", args: [e], type: STRING, loc: e.loc }; + // String(date) / `${date}` / "" + date: Date's ToPrimitive prefers + // toString(). + if (e.type.kind === "date") + return { kind: "libCall", fn: "date.toString", args: [e], type: STRING, loc: e.loc }; if (e.type.kind === "regex") { return { kind: "regexIntrinsic", @@ -9256,7 +9433,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]!); @@ -9279,7 +9456,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) ?? @@ -9325,7 +9502,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", @@ -9506,6 +9683,11 @@ export function lowerAsExpression( return inner; } } + // The global object: the checked extraction would build a separate + // value, so the assertion keeps the object and its member operations + // reach it (assertion-identity.ts). + if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) + return inner; if ( target.kind === "void" || !canDynCheckTo( @@ -10571,6 +10753,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr op === ts.SyntaxKind.ExclamationEqualsEqualsToken ) { const test = + lowerConstructorGlobalTypeofTest(lowerer, expr, loc) ?? lowerWebAssemblyTypeofTest(lowerer, expr, loc) ?? lowerErrorCodeTypeofTest(lowerer, expr, loc) ?? lowerer.lowerCaughtTypeofTest(expr, loc) ?? @@ -12217,7 +12400,7 @@ function aliasTypeofArmOf( const a = strip(a0); if (!ts.isIdentifier(a)) continue; const aliasSym = lowerer.resolveValueSymbol(a); - const aliasDecl = aliasSym ? lowerer.checker.valueDeclarationOf(aliasSym) : undefined; + const aliasDecl = aliasSym ? aliasSym.valueDeclaration : undefined; if ( !aliasSym || !aliasDecl || @@ -12231,7 +12414,7 @@ function aliasTypeofArmOf( if (!ts.isIdentifier(opnd)) continue; if (!bindingNeverReassigned(lowerer, aliasSym, aliasDecl)) continue; const valSym = lowerer.resolveValueSymbol(opnd); - const valDecl = valSym ? lowerer.checker.valueDeclarationOf(valSym) : undefined; + const valDecl = valSym ? valSym.valueDeclaration : undefined; if (!valSym || !valDecl) continue; // v1: identifier PARAMETERS only — initialized at function entry, // before any alias can capture their typeof. @@ -12491,7 +12674,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; @@ -12513,9 +12696,7 @@ function lowerReplacedExportsRead( * rare enough to keep fenced). */ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null { const symbol = lowerer.resolveValueSymbol(ident); - const getter = symbol - ? lowerer.checker.declarationsOf(symbol).find(ts.isGetAccessorDeclaration) - : undefined; + const getter = symbol ? symbol.declarations.find(ts.isGetAccessorDeclaration) : undefined; if (!getter) return null; if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) { return null; @@ -12578,7 +12759,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, ); @@ -12793,6 +12974,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (blob) return blob; const keyObject = lowerKeyObjectInstanceof(lowerer, expr); if (keyObject) return keyObject; + const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr); + if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; } @@ -13011,9 +13194,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", @@ -13031,6 +13227,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 @@ -13329,6 +13534,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. */ @@ -13382,7 +13718,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; }); @@ -14237,11 +14573,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); @@ -14256,7 +14592,7 @@ function staticRegexPatternOf(lowerer: Lowerer, e: ts.Expression): string | null * destructured bindings, and unresolvable names. */ function constInitializerOf(lowerer: Lowerer, ident: ts.Identifier): ts.Expression | null { const sym = lowerer.resolveValueSymbol(ident); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null; if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0) @@ -14315,7 +14651,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; } @@ -14341,17 +14677,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 @@ -14372,7 +14708,7 @@ function matchProducerRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression // The for-of binding over a matchAll drain: `for (const m of // s.matchAll(re))` (or over a stored const rows). const sym = lowerer.resolveValueSymbol(expr); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if ( decl !== undefined && ts.isVariableDeclaration(decl) && @@ -14418,7 +14754,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)) { @@ -14432,17 +14768,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; } @@ -14471,15 +14807,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 ( @@ -14819,13 +15155,11 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). - !lowerer.checker - .declarationsOf(propSym) - .some( - (d) => - d.parent !== undefined && - (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), - ) + !propSym.declarations.some( + (d) => + d.parent !== undefined && + (ts.isClassDeclaration(d.parent) || ts.isClassExpression(d.parent)), + ) ) { const found = objLitGenericFnNodeOf(lowerer, propSym); if (!found) { @@ -14915,7 +15249,9 @@ export function lowerUnionProperty( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (lowerer.chainBlocked(expr)) return null; - const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); + const receiverIr = + structuralReadType(lowerer, expr.expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (receiverIr?.kind !== "union") return null; // Lower the receiver first: its runtime value can still carry the full // binding union even when the checker has removed several variants. @@ -15122,6 +15458,62 @@ export function lowerUnionProperty( ); } +/** A number-keyed read in JavaScript on an array that may be undefined or + * null outside an optional chain (`m[1]` on a `RegExpExecArray | null` + * that an earlier statement ruled non-null without narrowing it): the + * array arm is an ordinary JavaScript element read, so a hole, a present + * undefined (a regex group that did not participate) or an invalid index + * answers undefined, as lowerSafeIndexRead does for a plain array. The + * unit arms keep the keyed read's answer. The receiver and key evaluate + * once, in source order. Null when the union is not exactly one array arm + * beside unit arms; optional-chain tails and TypeScript keep the keyed + * read. */ +function lowerOptionalArrayIndexRead( + lowerer: Lowerer, + expr: ts.ElementAccessExpression, + value: IrExpr, + key: IrExpr, +): IrExpr | null { + if ( + !isJsSourceFile(expr.getSourceFile()) || + expr.questionDotToken || + hasOptionalChainGuard(expr.expression) + ) + return null; + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms) return null; + const present = arms.filter((arm) => !isUnitType(arm)); + const arm = present[0]; + if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null; + const tag = arms.indexOf(arm); + const loc = locOf(expr); + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string): IrExpr => { + if (e.kind === "varRef" || e.kind === "numLit") return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null; + const recv = stable(value, "%indexReceiver"); + const index = stable(key, "%indexKey"); + const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc }; + const read = lowerSafeIndexRead(lowerer, narrowed, index, loc); + if (read === null) return null; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc }, + then: lowerer.coerceToExpected(read, keyed.type), + else_: keyed, + type: keyed.type, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc }; +} + /** The unionDisc generalization: a keyed read on a union receiver whose * arms answer DIFFERENT (but joinable) types. Each arm contributes its * declared answer — an array element's type (number keys), a declared field's type (literal keys), an @@ -15347,14 +15739,12 @@ function classFieldTarget( ); if ( property && - lowerer.checker - .declarationsOf(property) - .some( - (declaration) => - ts.isPropertyAccessExpression(declaration) || - ts.isBinaryExpression(declaration) || - ts.isElementAccessExpression(declaration), - ) + property.declarations.some( + (declaration) => + ts.isPropertyAccessExpression(declaration) || + ts.isBinaryExpression(declaration) || + ts.isElementAccessExpression(declaration), + ) ) { return { container: "dynamic", @@ -15558,7 +15948,7 @@ export function fieldTarget( // fences: JS finds the prototype function, never an overflow miss. const canonicalized = (): boolean => { if (!nameSym) return false; - const nameDecls = lowerer.checker.declarationsOf(nameSym); + const nameDecls = nameSym.declarations; if (!nameDecls.length) return false; if (!nameDecls.every((d) => ts.isPropertySignature(d))) return false; const declared = lowerer.mapTypeOf(lowerer.typeOf(access)); @@ -15631,7 +16021,7 @@ export function uniqueSymbolKeyOf( // which a runtime-identity symbol value can never satisfy. if (!(t.flags & (ts.TypeFlags.UniqueESSymbol | ts.TypeFlags.ESSymbol))) return null; const sym = lowerer.resolveValueSymbol(key); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl)) return null; if (!(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const)) return null; // Module level only: a unique-symbol const inside a FUNCTION is a @@ -15644,10 +16034,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)) @@ -16182,7 +16572,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, @@ -16195,7 +16585,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 @@ -16260,18 +16650,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))) { @@ -16280,7 +16670,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" @@ -16708,7 +17098,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr const recvType = lowerer.typeOf(access.expression); if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); - const decls = member ? lowerer.checker.declarationsOf(member) : []; + const decls = member ? member.declarations : []; return ( decls.length > 0 && decls.every((d) => { diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index b6d0a9863..0ef601672 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -59,7 +59,7 @@ function declareCallReceiver( export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; - const decl = symbol ? lowerer.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; return ( !!decl && ts.isFunctionDeclaration(decl) && @@ -171,10 +171,10 @@ export function lowerLambda( // A generic lambda used as a value has no per-call instantiation: it // lowers once with its type parameters bound to `unknown`, the erased // instance JavaScript itself runs. - if (node.typeParameters?.length && !lowerer.erasingLambdas.has(node)) { + if (node.typeParameters?.nodes.length && !lowerer.erasingLambdas.has(node)) { const previous = lowerer.typeParamBindings; const bindings = new Map(previous ?? []); - for (const parameter of node.typeParameters) { + for (const parameter of node.typeParameters.nodes) { const symbol = lowerer.checker.getSymbolAtLocation(parameter.name); if (symbol) bindings.set(symbol, DYN); } @@ -253,7 +253,7 @@ export function lowerLambda( // the lifted body returns the inner T (a `return v` fulfills with v). const isAsync = !ts.isAccessor(node) && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = node.asteriskToken !== undefined; if (isAsync && !isGenerator && funcType.ret.kind !== "promise") lowerer.badType(node, lowerer.typeOf(node)); @@ -303,11 +303,11 @@ export function lowerLambda( const receiver = (checkedReceiver || isJsSourceFile(node.getSourceFile()) || - node.parameters.some(isThisParameter)) && + node.parameters.nodes.some(isThisParameter)) && !ts.isArrowFunction(node) ? declareCallReceiver(lowerer, node, isGenerator) : null; - const { params, prologue } = lowerer.declareParams(node.parameters, shapes); + const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -323,7 +323,7 @@ export function lowerLambda( let body: IrStmt[]; if (ts.isBlock(node.body!)) { - body = lowerer.lowerStmts(node.body!.statements); + body = lowerer.lowerStmts(node.body!.statements.nodes); } else { // Bare-expression arrow body: `x => e` is `x => { return e; }` // (or an expression statement when the signature returns void — or @@ -494,10 +494,10 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.push(ctx); try { const receiver = - decl.parameters.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; - const { params, prologue } = lowerer.declareParams(decl.parameters, sig.params); + const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); if (receiver) prologue.unshift(receiver); declareFunctionArguments( lowerer, @@ -506,7 +506,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I params, prologue, locOf(decl), - sig.params.length > decl.parameters.length && + sig.params.length > decl.parameters.nodes.length && (sig.params[sig.params.length - 1]!.mode === "dynRest" || sig.params[sig.params.length - 1]!.mode === "arguments"), ); @@ -518,7 +518,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I "function declarations whose block body the frontend cannot locate", ); } - const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements)]; + const body = [...prologue, ...lowerer.lowerStmts(bodyBlock.statements.nodes)]; appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(decl)); const fn: IrFunction = { name: sig.name, diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index a9b467a6a..b1d0a1aa5 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) { @@ -203,14 +203,12 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression const member = lowerer.checker.getSymbolAtLocation(access.name); return ( member !== undefined && - lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + member.declarations.some( + (d) => + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ); } @@ -268,28 +266,24 @@ export function isSettledIteratorValueRead( const member = lowerer.checker.getSymbolAtLocation(node.name); if ( !member || - !lowerer.checker - .declarationsOf(member) - .some( - (d) => - ts.isPropertySignature(d) && - ts.isInterfaceDeclaration(d.parent) && - (d.parent.name.text === "IteratorYieldResult" || - d.parent.name.text === "IteratorReturnResult") && - lowerer.isStdlibFile(d.getSourceFile()), - ) + !member.declarations.some( + (d) => + ts.isPropertySignature(d) && + ts.isInterfaceDeclaration(d.parent) && + (d.parent.name.text === "IteratorYieldResult" || + d.parent.name.text === "IteratorReturnResult") && + lowerer.isStdlibFile(d.getSourceFile()), + ) ) return false; const receiver = lowerer.typeOf(node.expression); const stdlib = (sym: ts.Symbol | undefined): boolean => sym !== undefined && - lowerer.checker - .declarationsOf(sym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), - ); + sym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && + lowerer.isStdlibFile(d.getSourceFile()), + ); const alias = receiver.getAliasSymbol(); const symbol = receiver.getSymbol(); if ( @@ -379,7 +373,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..245d1b676 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -10,6 +10,7 @@ import { DYN, F64, type IrExpr, + type IrFunction, type IrStmt, type IrType, RUNTIME_ERROR_CLASSES, @@ -20,6 +21,9 @@ import { type SrcLoc, canConvertToDyn, canDynCheckTo, + classDynViewSupported, + DYN_CLASS_PROPERTIES, + isClassOwnEnumerableFieldName, isDirentShape, isDynTypedRefType, recordTextCodecClass, @@ -220,9 +224,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 +258,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 +352,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 +379,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 +480,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 +545,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") && @@ -942,6 +993,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { // Object.keys' declared order (SEMANTICS.md 36's stance). Retained // reachability can settle this shape after the helper is first // interned, so rebuild the field walk once metadata is final. + lowerer.keyOrder.observe(t); rebuildShapeOrderBody = (): void => { const current = lowerer.shapes.get(t.shapeId) ?? shape; const order = current.declaredOrder ?? current.fields.map((f) => f.name); @@ -1184,7 +1236,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; @@ -1292,7 +1344,10 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { prependCycleGuard(); } - const fn = { + // Typed as the IR function itself: the finalizer below replaces the + // body of the very object liftedFns holds (natively, a differently typed + // literal would be copied into the list). + const fn: IrFunction = { name, params: [ { localId: "v.0", name: "v", type: t }, @@ -1304,6 +1359,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 +1613,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`, @@ -1700,7 +1773,7 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo const base = cls.base ? ` extends ${cls.base.def.name.replace(/^%/, "")}` : ""; return strLit(`[class ${cls.def.name}${base}]`, loc); } - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return null; if (ts.isFunctionDeclaration(decl) && decl.name) { return strLit(`[Function: ${decl.name.text}]`, loc); @@ -1721,14 +1794,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 +2052,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 +2399,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 +2431,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 +2445,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 +2486,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 +2531,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 +2545,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 +2564,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 +2606,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 +2689,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..90789b4a0 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; } @@ -223,7 +223,7 @@ export function mixinFnOfCallee(lowerer: Lowerer, callee: ts.Expression): MixinF let sym = lowerer.checker.getSymbolAtLocation(callee) ?? null; if (sym && sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined) return null; // overloads / merged declarations const d = decls[0]; if (ts.isFunctionDeclaration(d)) return mixinFnShapeOf(lowerer, d); @@ -348,7 +348,7 @@ export function mixinResultBindingClassOf( sym: ts.Symbol | null | undefined, ): ClassInfo | null { if (!sym) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length !== 1 || decls[0] === undefined || !ts.isVariableDeclaration(decls[0])) return null; const decl = decls[0]; @@ -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 ?? ""}`, @@ -541,7 +541,7 @@ export function mixinIntersectionInstanceType(lowerer: Lowerer, widened: ts.Type const plainParts: string[] = []; for (const part of parts) { const sym = part.getSymbol(); - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; const node = decl && (ts.isClassDeclaration(decl) || ts.isClassExpression(decl)) && @@ -611,7 +611,8 @@ function pinnedMixinCallPosition(call: ts.CallExpression): boolean { n = p, p = p.parent ) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if (ts.isVariableDeclaration(p) && p.initializer === n) { return ts.isVariableDeclarationList(p.parent) && (p.parent.flags & ts.NodeFlags.Const) !== 0; } @@ -629,12 +630,13 @@ function staticsEvalStatementOf(call: ts.CallExpression): ts.Statement | null { let n: ts.Node = call; for (let p: ts.Node | undefined = n.parent; p !== undefined; n = p, p = p.parent) { if (ts.isParenthesizedExpression(p)) continue; - if (ts.isCallExpression(p) && p.arguments.length === 1 && p.arguments[0] === n) continue; + if (ts.isCallExpression(p) && p.arguments.nodes.length === 1 && p.arguments.nodes[0] === n) + continue; if ( ts.isVariableDeclaration(p) && p.initializer === n && ts.isVariableDeclarationList(p.parent) && - p.parent.declarations.length === 1 && + p.parent.declarations.nodes.length === 1 && ts.isVariableStatement(p.parent.parent) && ts.isSourceFile(p.parent.parent.parent) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index 2022d8232..4f446bb89 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -657,7 +657,7 @@ export function lowerMockFn( const fields = mockContextFields(lowerer, ctxType!)!; const { fn } = fields; requireUndefinedResultArm(lowerer, fields, call, what); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -717,7 +717,7 @@ export function lowerMockMethod( tracker: MockTracker, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) { lowerer.noLowering( `${what} with ${args.length} arguments`, @@ -844,8 +844,8 @@ export function lowerMockTrackerCall( member: "restoreAll" | "reset", tracker: MockTracker, ): IrExpr { - if (call.arguments.length > 0) { - lowerer.noLowering(`MockTracker.${member} with ${call.arguments.length} arguments`, call); + if (call.arguments.nodes.length > 0) { + lowerer.noLowering(`MockTracker.${member} with ${call.arguments.nodes.length} arguments`, call); } const fn = tracker ? member === "reset" @@ -893,7 +893,7 @@ export function lowerMockTimersCall( sym: ts.Symbol | null, ): IrExpr { const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; const optionalNumber = (fallback: number): IrExpr[] => { if (args.length > 1) lowerer.noLowering(`${what}.${member} with ${args.length} arguments`, call); @@ -935,9 +935,9 @@ export function lowerMockTimersCall( * (1 = `now` given, 2 = it is a Date). */ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: string): IrExpr[] { const loc = locOf(call); - const options = call.arguments[0]; - if (call.arguments.length > 1) { - lowerer.noLowering(`${what}.enable with ${call.arguments.length} arguments`, call); + const options = call.arguments.nodes[0]; + if (call.arguments.nodes.length > 1) { + lowerer.noLowering(`${what}.enable with ${call.arguments.nodes.length} arguments`, call); } if (options === undefined) { return [strLit(MOCK_TIMERS_ALL_APIS, loc), numLit(0, loc), numLit(0, loc)]; @@ -953,7 +953,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: let listed = false; let now: IrExpr = numLit(0, loc); let flags = 0; - for (const prop of options.properties) { + for (const prop of options.properties.nodes) { // `{ apis: [...] }`, `{ "now": n }` and the `{ now }` shorthand. const init = ts.isPropertyAssignment(prop) ? prop.initializer @@ -974,7 +974,7 @@ function lowerMockTimersOptions(lowerer: Lowerer, call: ts.CallExpression, what: ); } listed = true; - for (const el of list.elements) { + for (const el of list.elements.nodes) { if (!ts.isStringLiteralLike(el)) { lowerer.noLowering( `${what}.enable with a non-literal API name`, @@ -1046,7 +1046,7 @@ export function lowerMockContextCall( const { fn, calls, once, onceEntry } = mockContextFields(lowerer, ctxType)!; const loc = locOf(call); const member = access.name.text; - const args = call.arguments; + const args = call.arguments.nodes; const arity = (min: number, max: number = min) => { if (args.length < min || args.length > max || args.some(ts.isSpreadElement)) lowerer.noLowering(`MockFunctionContext.${member} with ${args.length} arguments`, call); diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 128652bd2..71ea52f75 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -93,6 +93,7 @@ import { import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; +import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, @@ -113,6 +114,7 @@ import { isVarDeclared, numericIteratorSourceOf, provenanceElidedConstDecl, + stdlibBufferVarAlias, } from "./lower-stmts.js"; import { objectFactoryGlobalType } from "./object-factory-new.js"; import { streamClassAliasDecl } from "./lower-stream.js"; @@ -144,6 +146,7 @@ import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; import { jsHoldsDynObject } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; /** One file's declarations, split for collection and init-body lowering. */ export interface FileParts { @@ -243,7 +246,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 +296,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 +370,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 +388,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 +443,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 +518,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 +528,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 +549,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 +636,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,12 +651,10 @@ 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 - .declarationsOf(modSym) - .some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); + modSym.declarations.some((d) => ts.isSourceFile(d) && !d.isDeclarationFile); const builtin = canonicalBuiltinModule(spec); const res = ownModule ? ({ kind: "program-module" } as const) @@ -684,14 +685,14 @@ 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; const nameSym = lowerer.checker.getSymbolAtLocation(clause.name); if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue; const target = lowerer.checker.getAliasedSymbol(nameSym); - const jsonSf = lowerer.checker.declarationsOf(target)[0]?.getSourceFile(); + const jsonSf = target.declarations[0]?.getSourceFile(); if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue; try { const tsType = lowerer.typeOf(clause.name); @@ -1053,7 +1054,7 @@ function checkedGlobalReference(lowerer: Lowerer, expression: ts.Expression): bo ) return false; const root = lowerer.checker.getSymbolAtLocation(expression.expression); - const namespace = root && lowerer.checker.declarationsOf(root).some(ts.isNamespaceImport); + const namespace = root && root.declarations.some(ts.isNamespaceImport); return ( (namespace || lowerer.cjsLocalModuleBindingOf(expression.expression)) && lowerer.globalOf(expression.name)?.type.kind === "dyn" @@ -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; @@ -1261,7 +1262,7 @@ export function collectGlobals( if (!symbol) continue; if (symbol.flags & ts.SymbolFlags.Alias) { const target = lowerer.checker.getAliasedSymbol(symbol); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -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; @@ -1370,8 +1371,7 @@ export function collectGlobals( if (vG) { if (!vG.mutable) { lowerer.globalsBySymbol.set(symbol, vG); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, vG); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, vG); return; } const g: IrGlobal = { @@ -1381,8 +1381,7 @@ export function collectGlobals( mutable: false, }; lowerer.globalsBySymbol.set(symbol, g); - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); return; } @@ -1392,8 +1391,7 @@ export function collectGlobals( lowerer.globalsBySymbol.set(symbol, g); // Importer aliases resolve to a DISTINCT late-bound symbol with // the same declaration — key the node too (globalOf's fallback). - for (const d of lowerer.checker.declarationsOf(symbol)) - lowerer.globalsByDeclNode.set(d, g); + for (const d of symbol.declarations) lowerer.globalsByDeclNode.set(d, g); lowerer.globalsList.push(g); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -1470,7 +1468,7 @@ export function collectGlobals( !( requireSpecOf(rhs) !== null && inlineProgramRequireOf(lowerer, rhs) !== null && - !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member") ) && cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { @@ -1501,7 +1499,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)) { @@ -1536,7 +1534,7 @@ export function collectGlobals( ) continue; const target = lowerer.checker.getAliasedSymbol(aliasSym); - const vd = lowerer.checker.valueDeclarationOf(target); + const vd = target.valueDeclaration; const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && @@ -1579,8 +1577,8 @@ export function collectGlobals( if ( isJsSourceFile(sf) && (isDeadTscHelperDecl(lowerer, stmt) || - (stmt.declarationList.declarations.length === 1 && - tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + (stmt.declarationList.declarations.nodes.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations.nodes[0]!) !== null)) ) { continue; } @@ -1595,14 +1593,14 @@ export function collectGlobals( isRequireStatement(stmt) && !( ts.isVariableStatement(stmt) && - stmt.declarationList.declarations.some( + stmt.declarationList.declarations.nodes.some( (decl) => isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl), ) ) ) { if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) { - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } @@ -1614,7 +1612,7 @@ export function collectGlobals( const isLetList = (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; - for (const decl of stmt.declarationList.declarations) { + for (const decl of stmt.declarationList.declarations.nodes) { for (const element of requireNestedPatternsOf(decl)) { for (const nameNode of boundIdentifiersOf(element.name!)) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); @@ -1646,7 +1644,8 @@ export function collectGlobals( // Node's ReferenceError at the access (lower-exprs), and the // statement itself lowers to nothing. if ( - ts.getCombinedModifierFlags(stmt.declarationList.declarations[0]!) & ts.ModifierFlags.Ambient + ts.getCombinedModifierFlags(stmt.declarationList.declarations.nodes[0]!) & + ts.ModifierFlags.Ambient ) continue; const list = stmt.declarationList; @@ -1661,8 +1660,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 @@ -1894,6 +1893,7 @@ export function collectGlobals( // same test). const stableStdlibAlias = isConst || + stdlibBufferVarAlias(lowerer, decl) || ((list.flags & ts.NodeFlags.Let) !== 0 && ts.isIdentifier(decl.name) && stdlibGlobalAliasNameOf(lowerer, decl.initializer) !== null && @@ -1918,7 +1918,8 @@ export function collectGlobals( decl.initializer && !decl.type && !hasJsTypeAnnotation(decl) && - !lowerer.mapTypeOf(lowerer.typeOf(nameNode)) + (!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) || + isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode))) ) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol && !lowerer.globalsBySymbol.has(symbol)) { @@ -1969,6 +1970,30 @@ export function collectGlobals( } continue; } + // A file-scope binding of an assertion of the global object + // (`const state = globalThis as { [KEY]?: State }`): the binding + // holds that object, so writes through it reach the object other + // modules read (assertion-identity.ts). + const asserted = decl.initializer + ? keptIdentityAssertion(lowerer, decl.initializer) + : undefined; + if (nameNode === decl.name && !decl.type && asserted) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${nsPrefix}${nameNode.text}`, + name: nameNode.text, + type: DYN, + mutable: isLet, + source: bindingSource(nameNode), + ...(!isVarDeclared(decl) ? { tdz: true as const } : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + continue; + } // A JS file-scope evolving ARRAY (`const mustCallChecks = [];` // — test/common's exit-accounting ledger): the strict type // (any[]) has no mapping, but the VALUE is the dyn array the @@ -2010,7 +2035,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.some( + decl.initializer.properties.nodes.some( (p) => !ts.isSpreadAssignment(p) && (ts.isMethodDeclaration(p) || @@ -2057,7 +2082,7 @@ export function collectGlobals( nameNode === decl.name && decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && - decl.initializer.properties.every((p) => { + decl.initializer.properties.nodes.every((p) => { if (ts.isSpreadAssignment(p)) { return ( ts.isIdentifier(p.expression) && @@ -2181,7 +2206,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 @@ -2247,8 +2272,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) ? lowerer.checker.getResolvedSignature(decl.initializer) : undefined; - const callDeclaration = - callSignature && lowerer.checker.signatureDeclaration(callSignature); + const callDeclaration = callSignature && callSignature.declaration; const callType = callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) ? lowerer.mapTypeOf(lowerer.typeOf(nameNode)) @@ -2274,7 +2298,7 @@ export function collectGlobals( !hasJsTypeAnnotation(decl) && decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) && - !decl.initializer.typeParameters?.length + !decl.initializer.typeParameters?.nodes.length ) { type = lowerer.lambdaSignature(decl.initializer).funcType; } @@ -2348,7 +2372,7 @@ export function collectGlobals( ts.isCallExpression(decl.initializer) && ts.isPropertyAccessExpression(decl.initializer.expression) && lowerer.stdlibGlobalMember(decl.initializer.expression, "Object") === "assign" && - decl.initializer.arguments.some( + decl.initializer.arguments.nodes.some( (arg) => ts.isIdentifier(arg) && lowerer.globalOf(arg)?.type.kind === "dyn", ) ) { @@ -2601,7 +2625,7 @@ function collectNestedVarGlobals( return undefined; }); for (const list of lists) { - for (const decl of list.declarations) { + for (const decl of list.declarations.nodes) { for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { @@ -2707,7 +2731,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) }; @@ -2876,11 +2900,16 @@ export function lowerFileInit( // a function called above the declaration reads `undefined`, exactly // Node); checked-dynamic var globals hold the dyn undefined the same // way. After the guard: a cache-hit revisit must not reset them. + // A union slot the body retypes checked-dynamic (lowerVarDecl: a + // JavaScript var initialized with an existing checked-dynamic object) + // gets the dyn undefined once the body has lowered. + const unionEntryInits: { at: number; g: IrGlobal }[] = []; for (const g of lowerer.varGlobalEntryInits.get(sf) ?? []) { if (g.type.kind !== "union" && g.type.kind !== "dyn") continue; const wrapped = g.type.kind === "dyn" ? dynUndefinedExpr(loc0) : lowerer.unassignedSlotInit(g.type, loc0); if (wrapped) { + if (g.type.kind === "union") unionEntryInits.push({ at: header.length, g }); // 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. @@ -2975,6 +3004,16 @@ export function lowerFileInit( ); at++; } + for (const { at, g } of unionEntryInits) { + if (g.type.kind !== "dyn") continue; + body[at] = { + kind: "assign", + localId: g.id, + value: dynUndefinedExpr(loc0), + ...(g.initFlag !== undefined ? { initializes: true as const } : {}), + loc: loc0, + }; + } const moduleRef = cjsModuleRef(lowerer, sf); if (moduleRef !== null) { const guardedBody = body.splice(guardId !== undefined ? 2 : 0); @@ -3132,6 +3171,7 @@ function rememberEvaluationError( export function buildMain(lowerer: Lowerer): IrFunction { const loc: SrcLoc = { file: lowerer.entry.fileName, start: 0, end: 0 }; const body: IrStmt[] = [ + ...lowerer.mainPrelude, ...cjsModuleRegistryPrelude(lowerer, loc), ...handleEventReaderPrelude(lowerer, loc), ]; diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 69d050510..23b7d0f57 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) || @@ -96,7 +96,7 @@ function nsSymbolRuntimeKind( if (!(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; let sawTypeOnly = false; let sawNamespace = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (!ts.isModuleDeclaration(d)) continue; sawNamespace = true; let body = d.body; @@ -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); } } @@ -189,7 +189,7 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { * null. The walk stops at class/function boundaries: a static member of a * class INSIDE a namespace is the class's business, not the namespace's. */ function nsBlockKindOfSymbol(lowerer: Lowerer, sym: ts.Symbol): "flattened" | "typeOnly" | null { - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { // TYPE declarations don't make a member a namespace VALUE: a class // static `C.B` whose name also carries `namespace C { export // interface B }` is the static's business — value references consult @@ -253,7 +253,7 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile } if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if ( sym.flags & ts.SymbolFlags.ValueModule && ts.isSourceFile(d) && @@ -322,7 +322,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi if (!sym || !(sym.flags & (ts.SymbolFlags.ValueModule | ts.SymbolFlags.NamespaceModule))) return null; if (lowerer.isStdlibSymbol(sym)) return null; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { // A namespace declaration, or the fundule/clazzdule merge partners @@ -427,7 +427,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id if (lowerer.trapBindings.has(sym)) return root; if (lowerer.isStdlibSymbol(sym)) return null; if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root; - const decls = lowerer.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; for (const d of decls) { if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null; @@ -608,7 +608,7 @@ export function ambientUndefinedFnSymbolOf( if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null; if (lowerer.isStdlibSymbol(sym)) return null; let sawFn = false; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners if (ts.isFunctionDeclaration(d)) { if (d.body) return null; // an implementation exists — not ambient-undefined @@ -656,7 +656,7 @@ function initExecutingPosition(node: ts.Node): boolean { function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number { let earliest = Infinity; - for (const d of lowerer.checker.declarationsOf(sym)) { + for (const d of sym.declarations) { if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart()); } return earliest; @@ -669,7 +669,7 @@ function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.Sour export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNode: ts.Node): void { const sym = lowerer.checker.getSymbolAtLocation(ident); if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return; - const aliasDecl = lowerer.checker.declarationsOf(sym).find(ts.isImportEqualsDeclaration); + const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration); if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return; if (!initExecutingPosition(refNode)) return; const sf = refNode.getSourceFile(); @@ -766,7 +766,7 @@ export function fenceCrossBlockNsRef( ) { if (lowerer.nsBlocks.has(q)) refBlocks.push(q); } - for (const d of lowerer.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { for ( let q: ts.Node | undefined = d.parent; q !== undefined && !ts.isSourceFile(q); diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index 292b38e87..fee25b10f 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -42,7 +42,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ if (!ts.isIdentifier(node) || node.text !== name) return false; if (lowerer.isStdlibGlobal(node, name)) return true; const symbol = lowerer.checker.getSymbolAtLocation(node); - const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + const decls = symbol ? symbol.declarations : []; const sourceShadow = decls.some( (decl) => !decl.getSourceFile().isDeclarationFile && @@ -204,16 +204,14 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { if ( symbol && (symbol.name === "Module" || symbol.name === "ScriptcModule") && - lowerer.checker - .declarationsOf(symbol) - .some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) ) { return true; } const property = lowerer.checker.getPropertyOfType(type, "filename"); const declaredByModule = property && - lowerer.checker.declarationsOf(property).some((decl) => { + property.declarations.some((decl) => { for (let parent: ts.Node | undefined = decl.parent; parent; parent = parent.parent) { if ( (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) && @@ -557,10 +555,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..0e1438fc9 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( @@ -5051,7 +5051,7 @@ function clientFnRefOf( * `const` declaration, or a parameter/let whose enclosing scope contains * no assignment or ++/-- targeting it. */ function neverReassigned(lowerer: Lowerer, sym: ts.Symbol): boolean { - const decl = lowerer.checker.declarationsOf(sym)[0]; + const decl = sym.declarations[0]; if (!decl) return false; if ( ts.isVariableDeclaration(decl) && @@ -5178,7 +5178,7 @@ function lowerHttpsModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -5279,7 +5279,7 @@ function lowerHttp2ModuleCall( bi: { module: string; member: string }, loc: SrcLoc, ): IrExpr { - const args = expr.arguments; + const args = expr.arguments.nodes; if (bi.member === "createServer") { // The REAL h2c server (scr_http2.c). An options object literal is // accepted and ignored (h2 session-tuning knobs; the honest-defaults @@ -5355,7 +5355,7 @@ function lowerHttp2ModuleCall( lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!)).length === 0 ) { if (ts.isObjectLiteralExpression(args[1]!)) { - for (const pr of (args[1] as ts.ObjectLiteralExpression).properties) { + for (const pr of (args[1] as ts.ObjectLiteralExpression).properties.nodes) { // Plain `name: value` and shorthand `{ ca }` both carry the // option; a shorthand's value is the same-named identifier. let name: string; @@ -5570,14 +5570,14 @@ function lowerHttp2ModuleCall( const ok = ts.isObjectLiteralExpression(whenTrue) && ts.isObjectLiteralExpression(whenFalse) && - whenFalse.properties.length === 0 && - whenTrue.properties.length === 1 && - ts.isPropertyAssignment(whenTrue.properties[0]!) && - (ts.isIdentifier(whenTrue.properties[0]!.name) || - ts.isStringLiteral(whenTrue.properties[0]!.name)) && - (whenTrue.properties[0]!.name as ts.Identifier | ts.StringLiteral).text === + whenFalse.properties.nodes.length === 0 && + whenTrue.properties.nodes.length === 1 && + ts.isPropertyAssignment(whenTrue.properties.nodes[0]!) && + (ts.isIdentifier(whenTrue.properties.nodes[0]!.name) || + ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) && + (whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text === "SNICallback" && - whenTrue.properties[0]!.initializer.getText() === condText; + whenTrue.properties.nodes[0]!.initializer.getText() === condText; if (!ok) { lowerer.noLowering( "createSecureServer options with a conditional spread", @@ -5595,7 +5595,7 @@ function lowerHttp2ModuleCall( `spreads must be inline object literals here; ${SNI_HINT}`, ); } - for (const p of inner.properties) lowerOption(p); + for (const p of inner.properties.nodes) lowerOption(p); return; } let initializer: ts.Expression | null; @@ -5671,14 +5671,14 @@ function lowerHttp2ModuleCall( e.kind === ts.SyntaxKind.FalseKeyword; const isLiteralObjectOfLiterals = (e: ts.Expression): boolean => ts.isObjectLiteralExpression(e) && - e.properties.every( + e.properties.nodes.every( (p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && isScalarLiteral(p.initializer), ); if (k === "settings" && initializer !== null && ts.isObjectLiteralExpression(initializer)) { - for (const setting of initializer.properties) { + for (const setting of initializer.properties.nodes) { if ( ts.isPropertyAssignment(setting) && ts.isIdentifier(setting.name) && @@ -5731,7 +5731,7 @@ function lowerHttp2ModuleCall( ); // An undocumented key, dropped like Node drops it. }; - for (const prop of optsNode.properties) lowerOption(prop); + for (const prop of optsNode.properties.nodes) lowerOption(prop); if (cert === null || key === null) { lowerer.noLowering( "createSecureServer without both cert and key", @@ -5846,7 +5846,7 @@ function lowerHttpClientCall( const c = lowerer.lowerCondition(binding!.cond); return binding!.trueSecure ? c : { kind: "unary", op: "!", operand: c, type: BOOL, loc }; }; - const args = expr.arguments; + const args = expr.arguments.nodes; if (args.length < 1 || args.length > 2) { lowerer.noLowering( `${member} with ${args.length} arguments`, @@ -5934,7 +5934,7 @@ function lowerHttpClientCall( let servername: IrExpr | null = null; /** maxHeaderSize: the response head's size bound. */ let maxHeaderSize: IrExpr | null = null; - for (const prop of optsNode.properties) { + for (const prop of optsNode.properties.nodes) { // Shorthand entries ({ port } for { port: port }) are the natural // spelling at these call sites — the shorthand's VALUE lowering // resolves the local binding (lowerShorthandValue), not the property @@ -6441,7 +6441,7 @@ function lowerClientHeadersOption(lowerer: Lowerer, node: ts.Expression): IrExpr const loc = locOf(node); if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -6493,8 +6493,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr if (!ts.isArrayLiteralExpression(node)) return lowerClientHeadersOption(lowerer, node); const loc = locOf(node); const flat: IrExpr[] = []; - for (const entry of node.elements) { - if (!ts.isArrayLiteralExpression(entry) || entry.elements.length !== 2) { + for (const entry of node.elements.nodes) { + if (!ts.isArrayLiteralExpression(entry) || entry.elements.nodes.length !== 2) { lowerer.noLowering( "addTrailers pair list", entry, @@ -6502,8 +6502,8 @@ function lowerHttpTrailersOption(lowerer: Lowerer, node: ts.Expression): IrExpr ); } const pair = entry as ts.ArrayLiteralExpression; - const name = pair.elements[0]!; - const value = pair.elements[1]!; + const name = pair.elements.nodes[0]!; + const value = pair.elements.nodes[1]!; if (ts.isSpreadElement(name) || ts.isSpreadElement(value)) { lowerer.noLowering( "addTrailers pair with spread", @@ -6528,7 +6528,7 @@ function lowerHttpClientMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setHeader") { requireStatementPosition(lowerer, call, "request.setHeader(...)"); if (args.length !== 2) @@ -7038,7 +7038,7 @@ function lowerHttpReqMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { requireStatementPosition(lowerer, call, "message.setTimeout(...)"); if (args.length < 1 || args.length > 2) @@ -7239,7 +7239,7 @@ function lowerHttpResMethodCall( if (!lowerer.isStdlibMember(access)) return null; const name = access.name.text; const loc = locOf(call); - const args = call.arguments; + const args = call.arguments.nodes; if (name === "setTimeout") { if (args.length < 1 || args.length > 2) { lowerer.noLowering( @@ -7298,7 +7298,7 @@ function lowerHttpResMethodCall( let pairs: IrExpr; if (ts.isObjectLiteralExpression(node)) { const elems: IrExpr[] = []; - for (const prop of node.properties) { + for (const prop of node.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( @@ -7542,7 +7542,7 @@ function lowerHttpResMethodCall( } const names: IrExpr[] = []; const values: IrExpr[] = []; - for (const prop of headersNode.properties) { + for (const prop of headersNode.properties.nodes) { const key = ts.isPropertyAssignment(prop) ? staticHeaderKeyOf(lowerer, prop) : null; if (!ts.isPropertyAssignment(prop) || key === null) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 9fbe87c3d..2f0e1c66f 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -177,15 +177,13 @@ function requestInitConstBacked( if (!symbol || seen.has(symbol)) return false; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return false; seen.add(symbol); - const declarations = lowerer.checker - .declarationsOf(symbol) - .filter( - (declaration): declaration is ts.VariableDeclaration => - ts.isVariableDeclaration(declaration) && - declaration.initializer !== undefined && - ts.isVariableDeclarationList(declaration.parent) && - (declaration.parent.flags & ts.NodeFlags.Const) !== 0, - ); + const declarations = symbol.declarations.filter( + (declaration): declaration is ts.VariableDeclaration => + ts.isVariableDeclaration(declaration) && + declaration.initializer !== undefined && + ts.isVariableDeclarationList(declaration.parent) && + (declaration.parent.flags & ts.NodeFlags.Const) !== 0, + ); return ( declarations.length > 0 && declarations.every((declaration) => @@ -214,8 +212,8 @@ function visitRequestInitConstSelectedValues( // a statically traced spread) hides earlier values exactly as it does at // runtime; a conditional/missing spread keeps the earlier contributor // in the possible-value set. - for (let i = expr.properties.length - 1; i >= 0; i--) { - const property = expr.properties[i]!; + for (let i = expr.properties.nodes.length - 1; i >= 0; i--) { + const property = expr.properties.nodes[i]!; if (ts.isSpreadAssignment(property)) { const defines = visitRequestInitConstSelectedValues( lowerer, @@ -238,7 +236,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; } @@ -300,7 +298,7 @@ function visitRequestInitConstSelectedValues( seen.add(symbol); let sawDeclaration = false; let defines = true; - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if ( ts.isVariableDeclaration(declaration) && declaration.initializer !== undefined && @@ -343,7 +341,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 { @@ -413,7 +411,7 @@ function fenceRequestInitProperty( : lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), key); if (!symbol || seen.has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isPropertyAssignment(declaration)) { fenceRequestInitValueInner(lowerer, declaration.initializer, seen); } else if (ts.isShorthandPropertyAssignment(declaration) && ts.isIdentifier(declaration.name)) { @@ -464,8 +462,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; @@ -533,7 +531,7 @@ function fenceRequestInitValueInner( if (!symbol || seen.has(symbol)) return; if (requestInitPropMutatedSymbols(lowerer).has(symbol)) return; seen.add(symbol); - for (const declaration of lowerer.checker.declarationsOf(symbol)) { + for (const declaration of symbol.declarations) { if (ts.isBindingElement(declaration)) { const source = requestInitConstBindingSource(lowerer, declaration); if (source !== null) { @@ -569,12 +567,12 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp if (call.questionDotToken) return null; const symbol = lowerer.resolveValueSymbol(callee); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - if (call.arguments.length < 1) return null; + if (call.arguments.nodes.length < 1) return null; const loc = locOf(call); - const initNode = call.arguments[1]; + const initNode = call.arguments.nodes[1]; if (initNode !== undefined) fenceRequestInitValue(lowerer, initNode); { - const inputNode = call.arguments[0]!; + const inputNode = call.arguments.nodes[0]!; const input = lowerer.lowerExpr(inputNode); let init: IrExpr; if (initNode === undefined) { @@ -608,7 +606,7 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp { kind: "varDecl", localId: inputLocal.id, init: input, loc }, { kind: "varDecl", localId: initLocal.id, init, loc }, ]; - for (const argument of call.arguments.slice(2)) { + for (const argument of call.arguments.nodes.slice(2)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on static fetch()", @@ -660,7 +658,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio if ( access.questionDotToken || (member !== "json" && member !== "text" && member !== "bytes" && member !== "arrayBuffer") || - call.arguments.length !== 0 + call.arguments.nodes.length !== 0 ) { return null; } @@ -1132,7 +1130,7 @@ export function fenceFetchObjectBinding(lowerer: Lowerer, pattern: ts.ObjectBind fenceFetchObjectMembers( lowerer, pattern, - pattern.elements.flatMap((element) => { + pattern.elements.nodes.flatMap((element) => { if (element.dotDotDotToken) return []; const member = fetchObjectBindingMemberName(lowerer, element); return member === null ? [] : [{ member, node: element }]; @@ -1151,7 +1149,7 @@ export function fenceFetchObjectAssignment( fenceFetchObjectMembers( lowerer, source, - target.properties.flatMap((property) => { + target.properties.nodes.flatMap((property) => { const member = fetchObjectAssignmentMemberName(lowerer, property); return member === null ? [] : [{ member, node: property }]; }), @@ -1195,7 +1193,7 @@ export function lowerFetchElementMethodCall( call.questionDotToken || !ts.isElementAccessExpression(call.expression) || call.expression.questionDotToken || - call.arguments.some((argument) => ts.isSpreadElement(argument)) + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) ) { return null; } @@ -1233,7 +1231,7 @@ export function lowerFetchElementMethodCall( const loc = locOf(call); const receiverLocal = lowerer.declareHiddenLocal("%fetchReceiver", DYN); const keyLocal = lowerer.declareHiddenLocal("%fetchMember", STRING); - const argumentValues = call.arguments.map((argument, index) => + const argumentValues = call.arguments.nodes.map((argument, index) => fetchMethodArgumentIsLive(resolved.owner, members, index) ? lowerLiveWebValue(lowerer, argument) : lowerer.lowerExprExpecting(argument, DYN), @@ -1272,7 +1270,7 @@ export function lowerFetchElementMethodCall( })); const staticResponsePromise: IrType | null = resolved.owner === "Response" && - call.arguments.length === 0 && + call.arguments.nodes.length === 0 && members.every((member) => member === "text" || member === "bytes") ? (() => { // The checker spells this call as @@ -1409,7 +1407,7 @@ function staticFirstArgWithSurplusArgs( first: IrExpr, ): IrExpr { const loc = locOf(call); - if (call.arguments.length <= 1) return first; + if (call.arguments.nodes.length <= 1) return first; const firstLocal = lowerer.declareHiddenLocal("%fetchArg", first.type); const firstRef: IrExpr = { kind: "varRef", @@ -1418,7 +1416,7 @@ function staticFirstArgWithSurplusArgs( loc, }; const stmts: IrStmt[] = [{ kind: "varDecl", localId: firstLocal.id, init: first, loc }]; - for (const argument of call.arguments.slice(1)) { + for (const argument of call.arguments.nodes.slice(1)) { if (ts.isSpreadElement(argument)) { lowerer.noLowering( "spread surplus arguments on the static fetch companion APIs", @@ -1472,14 +1470,14 @@ export function lowerStaticFetchCompanionCall( noteEventListeners(lowerer, loc); switch (call.expression.name.text) { case "timeout": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1490,7 +1488,7 @@ export function lowerStaticFetchCompanionCall( }), ); case "abort": - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "libCall", fn: "fetch.abortNow", @@ -1499,11 +1497,11 @@ export function lowerStaticFetchCompanionCall( loc, }; } - if (ts.isSpreadElement(call.arguments[0]!)) return null; + if (ts.isSpreadElement(call.arguments.nodes[0]!)) return null; return staticCallWithSurplusArgs( lowerer, call, - lowerLiveWebValue(lowerer, call.arguments[0]!), + lowerLiveWebValue(lowerer, call.arguments.nodes[0]!), (first) => ({ kind: "libCall", fn: "fetch.abortNow", @@ -1513,14 +1511,14 @@ export function lowerStaticFetchCompanionCall( }), ); case "any": - if (call.arguments[0] && ts.isSpreadElement(call.arguments[0])) { + if (call.arguments.nodes[0] && ts.isSpreadElement(call.arguments.nodes[0])) { return null; } return staticCallWithSurplusArgs( lowerer, call, - call.arguments[0] - ? lowerer.lowerExprExpecting(call.arguments[0], DYN) + call.arguments.nodes[0] + ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), (first) => ({ kind: "libCall", @@ -1537,9 +1535,9 @@ export function lowerStaticFetchCompanionCall( if ( root.text === "ReadableStream" && call.expression.name.text === "from" && - (!call.arguments[0] || !ts.isSpreadElement(call.arguments[0])) + (!call.arguments.nodes[0] || !ts.isSpreadElement(call.arguments.nodes[0])) ) { - if (!call.arguments[0]) { + if (!call.arguments.nodes[0]) { return { kind: "libCall", fn: "fetch.streamFrom", @@ -1548,7 +1546,7 @@ export function lowerStaticFetchCompanionCall( loc, }; } - let source = lowerer.lowerExpr(call.arguments[0]!); + let source = lowerer.lowerExpr(call.arguments.nodes[0]!); // A readonly tuple satisfies the fallback's readonly-array overload, // but maps to a monomorphic record in IR. Reuse the ordinary // tuple→array width conversion with the resolved generic parameter so @@ -1576,8 +1574,8 @@ export function lowerStaticFetchCompanionCall( source.type.kind === "dyn"; if (!preserve) { lowerer.noLowering( - `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments[0]!))} iterables`, - call.arguments[0]!, + `ReadableStream.from() over ${lowerer.checker.typeToString(lowerer.typeOf(call.arguments.nodes[0]!))} iterables`, + call.arguments.nodes[0]!, "arrays, Uint8Array, and strings are supported — spread other synchronous iterables into an array first: [...iterable]", ); } @@ -1607,7 +1605,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" @@ -1618,17 +1616,17 @@ export function lowerStaticAbortControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "AbortController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } const receiver = lowerer.lowerExpr(access.expression); if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); - const first = call.arguments[0] - ? lowerLiveWebValue(lowerer, call.arguments[0]) + const first = call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) : dynUndefinedExpr(loc); const args = [staticFirstArgWithSurplusArgs(lowerer, call, first)]; return lowerStaticFixedFetchMethodCall( @@ -1660,7 +1658,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 ) { @@ -1672,9 +1670,9 @@ export function lowerStaticReadableStreamControllerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultController" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1682,7 +1680,9 @@ export function lowerStaticReadableStreamControllerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1713,8 +1713,8 @@ export function lowerStaticReadableStreamCancelCall( const access = call.expression; if ( call.questionDotToken || - call.arguments.length > 1 || - call.arguments.some((argument) => ts.isSpreadElement(argument)) || + call.arguments.nodes.length > 1 || + call.arguments.nodes.some((argument) => ts.isSpreadElement(argument)) || (!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) || access.questionDotToken || staticResponseMemberName(lowerer, access) !== "cancel" @@ -1729,7 +1729,9 @@ export function lowerStaticReadableStreamCancelCall( if (receiver.type.kind !== "dyn") return null; const loc = locOf(call); const args = [ - call.arguments[0] ? lowerLiveWebValue(lowerer, call.arguments[0]) : dynUndefinedExpr(loc), + call.arguments.nodes[0] + ? lowerLiveWebValue(lowerer, call.arguments.nodes[0]) + : dynUndefinedExpr(loc), ]; return lowerStaticFixedFetchMethodCall( lowerer, @@ -1761,8 +1763,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 ) { @@ -1776,9 +1778,9 @@ export function lowerStaticAbortSignalListenerCall( const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -1786,7 +1788,7 @@ export function lowerStaticAbortSignalListenerCall( if (recv.type.kind !== "dyn") return null; const loc = locOf(call); noteEventListeners(lowerer, loc); - const args = call.arguments.map((arg, index) => + const args = call.arguments.nodes.map((arg, index) => index === 1 ? lowerLiveWebValue(lowerer, arg) : lowerer.lowerExprExpecting(arg, DYN), ); return lowerStaticFixedFetchMethodCall( @@ -1814,7 +1816,7 @@ export function lowerStaticAbortSignalListenerCall( export function lowerEventTargetNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "EventTarget" && name !== "Event" && name !== "CustomEvent") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); if (name === "EventTarget") noteEventListeners(lowerer, loc); @@ -1849,7 +1851,7 @@ export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some((argument) => ts.isSpreadElement(argument))) return null; const loc = locOf(expr); noteEventListeners(lowerer, loc); @@ -1884,7 +1886,7 @@ export function lowerStaticReadableStreamNew( } const symbol = lowerer.resolveValueSymbol(expr.expression); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.length > 1) return null; const loc = locOf(expr); @@ -1907,7 +1909,7 @@ export function lowerStaticReadableStreamNew( export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { const name = stdlibGlobalNameOf(lowerer, expr.expression); if (name !== "Request" && name !== "Headers") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; const loc = locOf(expr); @@ -1931,7 +1933,7 @@ export function lowerFetchWebNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) return null; return lowerResponseConstruction(lowerer, args, locOf(expr), false); } @@ -1944,8 +1946,9 @@ export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpressio const access = call.expression; if (access.questionDotToken || access.name.text !== "json") return null; if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null; - if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null; - return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true); + if (call.arguments.nodes.some(ts.isSpreadElement) || call.arguments.nodes.length === 0) + return null; + return lowerResponseConstruction(lowerer, call.arguments.nodes, locOf(call), true); } function lowerResponseConstruction( @@ -2110,7 +2113,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 ) { @@ -2122,9 +2125,9 @@ export function lowerStaticReadableStreamReaderCall( const symbol = receiverTs.getSymbol(); if ( symbol?.name !== "ReadableStreamDefaultReader" || - !lowerer.checker - .declarationsOf(symbol) - .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile())) + !symbol.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + ) ) { return null; } @@ -2152,7 +2155,7 @@ export function lowerStaticReadableStreamReaderCall( export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { if (call.expression.kind !== ts.SyntaxKind.ImportKeyword) return null; - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if (arg === undefined || !ts.isStringLiteralLike(arg)) { lowerer.unsupported( "SC1090", @@ -2161,7 +2164,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}`); @@ -2178,7 +2181,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression /** The checker-resolved program source behind one literal import(). */ function programImportTarget(lowerer: Lowerer, arg: ts.StringLiteralLike): ts.SourceFile | null { const modSym = lowerer.checker.getSymbolAtLocation(arg); - for (const d of modSym ? lowerer.checker.declarationsOf(modSym) : []) { + for (const d of modSym ? modSym.declarations : []) { if (ts.isSourceFile(d) && !d.isDeclarationFile) return d; } return null; @@ -2354,14 +2357,14 @@ export function lowerStdlibMethodCall( const sigMember = lowerer.stdlibGlobalMember(access, "AbortSignal"); if ( (sigMember === "timeout" || sigMember === "abort" || sigMember === "any") && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi(`'AbortSignal.${sigMember}'`, call); } } if ( lowerer.stdlibGlobalMember(access, "ReadableStream") === "from" && - call.arguments.every((argument) => !ts.isSpreadElement(argument)) + call.arguments.nodes.every((argument) => !ts.isSpreadElement(argument)) ) { lowerer.refuseUnsupportedApi("'ReadableStream.from'", call); } @@ -2374,10 +2377,10 @@ export function lowerStdlibMethodCall( // for undefined, instead of trusting the checker's number type. const mathArg = (a: ts.Expression): IrExpr => lowerToNumberArgument(lowerer, a); if (staticMath && name === "hypot") { - const elems = call.arguments.map((a) => + const elems = call.arguments.nodes.map((a) => ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : mathArg(a), ); - const spreads = call.arguments.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); + const spreads = call.arguments.nodes.flatMap((a, i) => (ts.isSpreadElement(a) ? [i] : [])); const packed: IrExpr = { kind: "arrayLit", elems, @@ -2387,7 +2390,7 @@ export function lowerStdlibMethodCall( }; return { kind: "libCall", fn: staticMath.fn, args: [packed], type: F64, loc }; } - if (staticMath && call.arguments.every((a) => !ts.isSpreadElement(a))) { + if (staticMath && call.arguments.nodes.every((a) => !ts.isSpreadElement(a))) { // Math.max/Math.min at ANY plain arity — Node's are variadic. The // spec's reduction is a left fold of the same NaN-poisoning // ±0-ordered scalar compare the two-arg form lowers to, so n @@ -2398,18 +2401,18 @@ export function lowerStdlibMethodCall( // arguments are the fold's seed: -Infinity for max, +Infinity for // min, exactly Node. if (name === "max" || name === "min") { - if (call.arguments.length === 0) { + if (call.arguments.nodes.length === 0) { return { kind: "numLit", value: name === "max" ? -Infinity : Infinity, type: F64, loc }; } - const args = call.arguments.map(mathArg); + const args = call.arguments.nodes.map(mathArg); let acc = args[0]!; for (let i = 1; i < args.length; i++) { acc = { kind: "libCall", fn: staticMath.fn, args: [acc, args[i]!], type: F64, loc }; } return acc; } - if (call.arguments.length === staticMath.arity) { - const args = call.arguments.map(mathArg); + if (call.arguments.nodes.length === staticMath.arity) { + const args = call.arguments.nodes.map(mathArg); return { kind: "libCall", fn: staticMath.fn, args, type: F64, loc }; } } @@ -2420,10 +2423,10 @@ export function lowerStdlibMethodCall( if ( isMath && (name === "max" || name === "min") && - call.arguments.length === 1 && - ts.isSpreadElement(call.arguments[0]!) + call.arguments.nodes.length === 1 && + ts.isSpreadElement(call.arguments.nodes[0]!) ) { - const spread = call.arguments[0]! as ts.SpreadElement; + const spread = call.arguments.nodes[0]! as ts.SpreadElement; let src = lowerer.lowerExpr(spread.expression); // JavaScript array call results retain checked storage so aliases stay // shared. This read-only fold can extract the inferred numeric array. @@ -2449,7 +2452,7 @@ export function lowerStdlibMethodCall( }; } const mathFn = isMath ? own(STDLIB_SURFACE.math.fns, name) : undefined; - if (mathFn && call.arguments.length === mathFn.args.length) { + if (mathFn && call.arguments.nodes.length === mathFn.args.length) { lowerer.refuseUnsupportedApi(`'Math.${name}'`, call); } const kind = lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind; @@ -2459,7 +2462,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); } @@ -2538,7 +2541,7 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres * npmPackageOf, also asked directly for import-alias bindings (whose * aliased symbol is the package's export). */ export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return null; if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null; return npmPackageNameOf(decls[0]!.getSourceFile().fileName); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 8ded9106b..2bdc6c672 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,3 +1,7 @@ +import { + matchAllRowsReadByElements, + storedMatchAllRowsReadByElements, +} from "./containers/match-row-uses.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; @@ -83,6 +87,7 @@ import { isJsSourceFile, isNodeEsmFile, locOf, + requireEarlyReference, requireSpecOf, } from "../program.js"; import { @@ -94,6 +99,7 @@ import { sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, + bufferNamespaceMember, stdlibGlobalNameOf, } from "./surfaces.js"; import { isNativeBuiltinValueInitializer } from "./lower-builtin-values.js"; @@ -187,10 +193,12 @@ import { import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { + jsHoldsDynObject, lowerEnvironmentKey, lowerNativeFunctionAssignment, lowerStrictEquality, } from "./lower-exprs.js"; +import { keptIdentityAssertion } from "./assertion-identity.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerUnionFieldWrite } from "./expressions/union-fields.js"; @@ -238,7 +246,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" @@ -342,8 +350,7 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit); const sym = t.getAliasSymbol() ?? t.getSymbol(); return ( - sym?.name === name && - lowerer.checker.declarationsOf(sym).some((d) => lowerer.isStdlibFile(d.getSourceFile())) + sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) ); }; return visit(lowerer.typeOf(node)); @@ -564,7 +571,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"); @@ -592,7 +599,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; @@ -838,7 +845,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) || @@ -906,7 +913,7 @@ const TDZ_KINDS = new Set([ * is empty. Mutable bindings also guard writes; only the declaration's * initializing assignment may leave the temporal dead zone. */ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): boolean { - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -986,8 +993,9 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b * lower the declaration under the owning statement list's context, then * 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; + const decl = symbol.valueDeclaration; + 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); @@ -1062,13 +1070,124 @@ function hostVariableDeclarationOf(name: ts.Identifier): ts.VariableDeclaration export function forOfVarTarget( lowerer: Lowerer, decl: ts.VariableDeclaration, + valueType?: IrType, ): { id: string; type: IrType } | null { if (!ts.isIdentifier(decl.name) || !isVarDeclared(decl)) return null; const symbol = lowerer.checker.getSymbolAtLocation(decl.name); if (!symbol) return null; const g = lowerer.globalsBySymbol.get(symbol); - if (g) return g; - return hoistVarBinding(lowerer, symbol, decl.name); + if (g) { + if (valueType?.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, decl.name, g.type)) + g.type = DYN; + return g; + } + return hoistVarBinding(lowerer, symbol, decl.name, valueType); +} + +/** JavaScript: whether an unannotated variable binding whose static type + * is `type` keeps a checked-dynamic value unconverted. The checker types + * such a binding by the object it receives (an element of an evolving + * `[]`, a `find` result, a member of a checked-dynamic object), but the + * value is an existing checked-dynamic object or array: a record- or + * array-typed slot would hold a converted copy, so writes through the + * binding would miss the object other references see, and `===` against + * them would fail. Block-scoped bindings with an identifier name keep such + * values by the same rule in lowerVarDecl (preservesObjectIdentity). */ +function jsBindingKeepsCheckedObject( + lowerer: Lowerer, + nameNode: ts.Identifier, + type: IrType, +): boolean { + if (!isJsSourceFile(nameNode.getSourceFile())) return false; + const host = hostVariableDeclarationOf(nameNode); + if (!host || host.type || hasJsTypeAnnotation(host)) return false; + const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type]; + return ( + arms.some((arm) => arm.kind === "record" || arm.kind === "array") && + arms.every((arm) => arm.kind === "record" || arm.kind === "array" || isUnitType(arm)) + ); +} + +/** Whether `node` mentions the binding `symbol` anywhere, nested functions + * included. */ +function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { + if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol; + return ( + node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) === + true + ); +} + +/** Whether `node` evaluates to an existing value rather than constructing + * one whose layout a destination directs: not a literal, `new`, function + * or class expression, nor a conditional with such an arm (an arm that + * is `null` or `undefined` constructs nothing). */ +function existingValueForm(node: ts.Expression, armOfConditional = false): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (ts.isConditionalExpression(node)) + return existingValueForm(node.whenTrue, true) && existingValueForm(node.whenFalse, true); + if ( + armOfConditional && + (node.kind === ts.SyntaxKind.NullKeyword || + (ts.isIdentifier(node) && node.text === "undefined")) + ) + return true; + return !( + ts.isObjectLiteralExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isNewExpression(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isClassExpression(node) || + ts.isLiteralExpression(node) || + node.kind === ts.SyntaxKind.NullKeyword + ); +} + +/** JavaScript `var x = e` whose binding could hold an existing + * checked-dynamic object (jsBindingKeepsCheckedObject): lowers `e` BEFORE + * minting the hoisted slot, as a block-scoped declaration does, so a + * checked-dynamic value gets a checked-dynamic slot instead of a converted + * copy. Other values coerce into the planned slot exactly as + * lowerExprExpecting would. Null when the declaration does not qualify: + * the slot already exists, the initializer is fresh construction whose + * layout the slot type directs (existingValueForm; a literal holding an + * existing checked-dynamic object qualifies), or it refers to the binding + * itself (hoist-before-init resolves that reference). */ +function lowerJsVarFromExisting( + lowerer: Lowerer, + decl: ts.VariableDeclaration, + symbol: ts.Symbol, +): IrStmt | null { + if (!ts.isIdentifier(decl.name) || !decl.initializer) return null; + if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null; + if (lowerer.bindingIn(lowerer.ctx, symbol)) return null; + let node: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) + node = node.expression; + // A literal holding an existing checked-dynamic object builds as a + // checked-dynamic literal without a destination (lowerArrayLiteral, + // lowerObjectLiteral): a record- or array-typed slot would copy it. + const holdsChecked = + (ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) && + jsHoldsDynObject(lowerer, node); + if (!holdsChecked && !existingValueForm(node)) return null; + const planned = varBindingType(lowerer, decl.name); + if (!planned || !jsBindingKeepsCheckedObject(lowerer, decl.name, planned)) return null; + if (mentionsSymbol(lowerer, node, symbol)) return null; + const value = lowerer.lowerExpr(node); + const local = hoistVarBinding( + lowerer, + symbol, + decl.name, + value.type.kind === "dyn" ? DYN : undefined, + ); + return { + kind: "assign", + localId: local.id, + value: lowerer.coerceExistingInto(node, value, local.type), + loc: locOf(decl), + }; } /** The `var` symbol's function-scoped binding TYPE — the declared type at @@ -1124,7 +1243,12 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul * a loop re-ASSIGNS the one binding per pass and never resets it — the * classic capture semantics fall out: closures made in a loop share the * single boxed binding, where `let` gets a fresh box per iteration. */ -function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Identifier): IrLocal { +function hoistVarBinding( + lowerer: Lowerer, + symbol: ts.Symbol, + nameNode: ts.Identifier, + valueType?: IrType, +): IrLocal { const existing = lowerer.ctx.hoistedVars.get(symbol); if (existing) return existing; // The parameter merge: the symbol already binds a function-root local. @@ -1133,7 +1257,15 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident lowerer.ctx.hoistedVars.set(symbol, bound); return bound; } - const type = varBindingType(lowerer, nameNode); + let type = varBindingType(lowerer, nameNode); + // The first value stored is an existing checked-dynamic object: keep it + // (jsBindingKeepsCheckedObject). A checked-dynamic slot holds undefined + // natively, so no runtime-optional widening applies. + const keepsChecked = + valueType?.kind === "dyn" && + type !== null && + jsBindingKeepsCheckedObject(lowerer, nameNode, type); + if (keepsChecked) type = DYN; if (!type) lowerer.badType(nameNode, lowerer.typeOf(nameNode)); const root = lowerer.activeStmtLists.find((e) => e.ctx === lowerer.ctx); if (!root) @@ -1150,7 +1282,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident }; lowerer.ctx.locals.push(local); lowerer.ctx.scopes[0]!.set(symbol, local); - noteEarlyReadVar(lowerer, symbol, local); + if (!keepsChecked) noteEarlyReadVar(lowerer, symbol, local); // A checked-dynamic slot holds the dyn undefined (a NULL dyn is a // trap); everything else rides unassignedSlotInit (interned union arm, @@ -1179,7 +1311,7 @@ function hoistVarBinding(lowerer: Lowerer, symbol: ts.Symbol, nameNode: ts.Ident export function predeclareForwardVar(lowerer: Lowerer, symbol: ts.Symbol): boolean { // Module-scope vars already have their global storage. if (lowerer.globalsBySymbol.has(symbol)) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isVariableDeclaration(decl) || !isVarDeclared(decl)) return false; const nameNode = ts.isIdentifier(decl.name) ? decl.name : null; if (!nameNode) { @@ -1254,7 +1386,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); @@ -1282,7 +1414,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(); @@ -1355,7 +1487,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); } @@ -1599,7 +1731,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). @@ -1761,7 +1893,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; @@ -1868,7 +2000,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); @@ -1885,7 +2017,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, @@ -1918,11 +2052,7 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool return false; }; const sym = widened.getAliasSymbol() ?? widened.getSymbol(); - if ( - sym && - isParseArgsDynTypeName(sym.name) && - lowerer.checker.declarationsOf(sym).some(declarationBelongs) - ) { + if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) { return true; } // A discriminant check erases the token alias and leaves one anonymous @@ -1930,7 +2060,44 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool // is the same stable signal the dotted-member bridge uses. return lowerer.checker .getPropertiesOfType(widened) - .some((p) => lowerer.checker.declarationsOf(p).some(declarationBelongs)); + .some((p) => p.declarations.some(declarationBelongs)); +} + +/** True when an `any` value comes from a JavaScript function's result: + * a call (possibly awaited) whose callee is declared in a JavaScript file, + * or a const bound to one. A TypeScript program importing a JavaScript + * package sees the package's inferred types (`jwtVerify` from jose + * answers `any`), and its object destructuring reads the members of that + * checked-dynamic value as JavaScript does — member reads on the same + * value already compile. Other `any` sources keep the fence. */ +function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; + if (ts.isCallExpression(expr)) { + const callee = ts.isPropertyAccessExpression(expr.expression) + ? expr.expression.name + : expr.expression; + if (!ts.isIdentifier(callee)) return false; + const symbol = lowerer.resolveValueSymbol(callee); + const declarations = symbol ? lowerer.checker.declarationsOf(symbol) : []; + return ( + declarations.length > 0 && + declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) + ); + } + if (ts.isIdentifier(expr) && depth === 0) { + const symbol = lowerer.resolveValueSymbol(expr); + const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + return ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.initializer !== undefined && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && + javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) + ); + } + return false; } /** Destructuring declarations, desugared as sugar over indexed/field @@ -1977,7 +2144,12 @@ export function lowerDestructuringDecl( lowerer.anyOrigin(decl.initializer) && !isJsSourceFile(decl.getSourceFile()) && init.type.kind !== "record" && - init.type.kind !== "array" + init.type.kind !== "array" && + !( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + javaScriptAnyOrigin(lowerer, decl.initializer, 0) + ) ) { lowerer.anyOpFence("destructuring", decl); } @@ -2126,11 +2298,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"; @@ -2203,8 +2375,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"); @@ -2225,7 +2397,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; @@ -2300,7 +2472,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) { @@ -2385,12 +2557,12 @@ 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, ) ? { kind: "libCall", - fn: "dyn.iterPack", + fn: "dyn.destructureRest", args: [srcRef(), { kind: "strLit", value: dynSpell ?? "", type: STRING, loc }], type: DYN, loc, @@ -2398,14 +2570,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) { @@ -2469,7 +2641,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) { @@ -2517,7 +2689,7 @@ export function lowerBindingPattern( // like the string case above. Element defaults keep a fence: they // run between iterator steps and could mutate the live collection. if (srcType.kind === "map" || srcType.kind === "set") { - const defaulted = pattern.elements.find( + const defaulted = pattern.elements.nodes.find( (el): el is ts.BindingElement => ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken, ); @@ -2550,7 +2722,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 @@ -2587,7 +2759,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 @@ -2634,12 +2806,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 = @@ -2727,7 +2899,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) { @@ -2736,7 +2908,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 @@ -2844,7 +3016,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) { @@ -2899,6 +3071,10 @@ export function lowerBindingPattern( lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + if (srcType.kind === "array" && isArrayPositionPattern(lowerer, pattern)) { + lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -2911,7 +3087,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 @@ -3203,7 +3379,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) { @@ -3229,19 +3405,8 @@ function lowerMatchResultObjectPattern( ); } let value: IrExpr; - if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) { - const index = Number(propName); - const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; - value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { - kind: "arrayGet", - arr: srcRef(), - index: position, - type: srcType.elem, - loc, - }; - if (el.initializer) { - value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out); - } + if (ARRAY_POSITION_KEY.test(propName)) { + value = arrayPositionBinding(lowerer, el, srcRef, srcType, Number(propName), out); } else if (propName === "index" || propName === "input") { if (el.initializer) { lowerer.unsupported( @@ -3287,6 +3452,83 @@ function lowerMatchResultObjectPattern( } } +/** A canonical array index key (the numeric positions an object pattern + * reads off an array). */ +const ARRAY_POSITION_KEY = /^(?:0|[1-9]\d{0,8})$/; + +/** The positional read `{ : x }` takes off an array: the element, + * undefined past the end, with the binding default applied. */ +function arrayPositionBinding( + lowerer: Lowerer, + el: ts.BindingElement, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + index: number, + out: IrStmt[], +): IrExpr { + const loc = locOf(el); + const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; + const value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { + kind: "arrayGet", + arr: srcRef(), + index: position, + type: srcType.elem, + loc, + }; + return el.initializer + ? arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out) + : value; +} + +/** Whether every element of an object pattern over an array names a + * 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.nodes.every((el) => { + if (el.name === undefined) return true; + if (el.dotDotDotToken) return false; + const prop = el.propertyName ?? el.name; + if ( + !ts.isIdentifier(prop) && + !ts.isComputedPropertyName(prop) && + !ts.isStringLiteralLike(prop) && + !ts.isNumericLiteral(prop) + ) + return false; + const name = patternKeyNameOf(lowerer, prop as ts.PropertyName); + return name !== null && (name === "length" || ARRAY_POSITION_KEY.test(name)); + }); +} + +/** An object pattern of positions and `length` over an array: each + * position is the read the equivalent array pattern makes, and `length` + * the array's length (always present, so its default never runs). */ +function lowerArrayPositionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + isLet: boolean, + out: IrStmt[], +): void { + 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 = + name === "length" + ? { + kind: "arrIntrinsic", + method: "length", + receiver: srcRef(), + args: [], + type: F64, + loc: locOf(el), + } + : arrayPositionBinding(lowerer, el, srcRef, srcType, Number(name), out); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Decode one non-rest object-assignment property into its static source * field, destination expression, and optional default initializer. */ function destructuringPropertyOf( @@ -3344,7 +3586,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 @@ -3356,7 +3598,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; @@ -3454,7 +3696,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( @@ -3910,7 +4152,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; @@ -4007,7 +4249,7 @@ function objectRestValue( ), }; }); - return { kind: "recordLit", fields, type: restT, loc }; + return lowerer.keyOrder.copy({ kind: "recordLit", fields, type: restT, loc }, srcType.shapeId); } /** The fresh record for `{ a, ...rest }` over a CLASS-INSTANCE source: @@ -4190,6 +4432,8 @@ export function bindPatternTarget( const symbol = lowerer.checker.getSymbolAtLocation(name); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (g) { + if (value.type.kind === "dyn" && jsBindingKeepsCheckedObject(lowerer, name, g.type)) + g.type = DYN; out.push({ kind: "assign", localId: g.id, @@ -4206,7 +4450,7 @@ export function bindPatternTarget( // here by declareParams) carry no block-scope flag either, but their // bindings are the parameters' own. if (symbol && hostVariableDeclarationOf(name) !== null && isVarDeclared(name)) { - const hoisted = hoistVarBinding(lowerer, symbol, name); + const hoisted = hoistVarBinding(lowerer, symbol, name, value.type); out.push({ kind: "assign", localId: hoisted.id, @@ -4221,7 +4465,7 @@ export function bindPatternTarget( value.type.kind === "dyn" && (() => { const mapped = lowerer.mapTypeOf(lowerer.typeOf(name)); - return mapped === null; + return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped); })(); let type = dynStays ? DYN @@ -4285,7 +4529,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)) { @@ -4299,7 +4543,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. @@ -4497,7 +4741,7 @@ export function lowerVarDecl( !isJsSourceFile(decl.getSourceFile()) && !decl .getSourceFile() - .statements.some( + .statements.nodes.some( (s) => ts.isImportDeclaration(s) || ts.isExportDeclaration(s) || @@ -4557,6 +4801,7 @@ export function lowerVarDecl( // alias plumbing (see stdlibGlobalAliasDecl), no storage, no code. const stableStdlibAlias = !isLet || + stdlibBufferVarAlias(lowerer, decl) || (ts.isVariableDeclarationList(decl.parent) && (decl.parent.flags & ts.NodeFlags.Let) !== 0 && ts.isIdentifier(decl.name) && @@ -4704,7 +4949,11 @@ export function lowerVarDecl( !decl.type && !hasJsTypeAnnotation(decl) && raw.type.kind === "dyn" && - (g.type.kind === "record" || g.type.kind === "object") + (g.type.kind === "record" || + g.type.kind === "object" || + (ts.isIdentifier(decl.name) && + jsBindingKeepsCheckedObject(lowerer, decl.name, g.type) && + (existingValueForm(decl.initializer) || jsHoldsDynObject(lowerer, decl.initializer)))) ) g.type = DYN; g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type); @@ -4751,6 +5000,8 @@ export function lowerVarDecl( // types already hold the interned undefined arm), and a declaration // re-run in a loop must not reset the persisting value. if (isVarDeclared(decl) && declSymbol) { + const fromExisting = lowerJsVarFromExisting(lowerer, decl, declSymbol); + if (fromExisting) return fromExisting; const local = hoistVarBinding(lowerer, declSymbol, decl.name); if (!decl.initializer) return null; const init = lowerer.lowerExprExpecting(decl.initializer, local.type); @@ -4814,7 +5065,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) || @@ -4911,9 +5162,16 @@ export function lowerVarDecl( lowerer.unions.get(inferredObjectType.unionId)?.arms.every(isUnitType))))) && !decl.type && !hasJsTypeAnnotation(decl); + // The binding of an assertion of the global object holds that object, + // whatever its asserted type (assertion-identity.ts). + const keepsAssertedObject = + init.type.kind === "dyn" && + !decl.type && + keptIdentityAssertion(lowerer, decl.initializer) !== undefined; if ( expandsObject || preservesObjectIdentity || + keepsAssertedObject || (isJsSourceFile(decl.getSourceFile()) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) @@ -4972,7 +5230,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]; @@ -5126,7 +5384,7 @@ export function lowerVarDecl( ? npmStaticOptionalSelfWriteType(lowerer, decl, settledType) : null; settledType = - expandsObject || preservesObjectIdentity + expandsObject || preservesObjectIdentity || keepsAssertedObject ? DYN : lowerer.runtimeOptionalBindingType(decl.name, settledType); if (optionalSelfWriteType !== null) settledType = optionalSelfWriteType; @@ -5312,7 +5570,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 = @@ -5423,7 +5681,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)) || @@ -5439,7 +5697,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; @@ -5549,7 +5807,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; @@ -5608,7 +5866,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; } @@ -5656,7 +5914,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); @@ -5672,7 +5930,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, ), }); @@ -5774,7 +6035,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) && @@ -5792,13 +6053,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, ), })); @@ -5889,12 +6150,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))) ); } @@ -6173,7 +6434,7 @@ function lowerCjsExportStatement( if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; const sf = cjs.expr.getSourceFile(); let root: IrGlobal | undefined; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const whole = cjsExportAssignmentOf(stmt); if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); @@ -6264,7 +6525,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 @@ -6391,10 +6652,7 @@ function lowerCjsSingleValueExport( // own statement already evaluated it), and a builtin global // (`module.exports = TypeError`) is the global itself: requirer // bindings alias straight through to it, so nothing is stored. - if ( - lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || - lowerer.isStdlibSymbol(sym) - ) { + if (sym.declarations.some((d) => ts.isSourceFile(d)) || lowerer.isStdlibSymbol(sym)) { return { kind: "block", body: [], loc }; } const g = lowerer.globalsBySymbol.get(sym); @@ -6513,10 +6771,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // positions keep the first-class-namespace fences. if (ts.isIdentifier(expr)) { const sym = lowerer.checker.getSymbolAtLocation(expr); - if ( - sym !== undefined && - lowerer.checker.declarationsOf(sym).some((d) => ts.isNamespaceImport(d)) - ) { + if (sym !== undefined && sym.declarations.some((d) => ts.isNamespaceImport(d))) { return { kind: "block", body: [], loc: locOf(expr) }; } } @@ -6864,7 +7119,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 @@ -7290,6 +7547,15 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (droppedPureStdlibStatement(lowerer, expr)) { return { kind: "block", body: [], loc: locOf(expr) }; } + if ( + stmtNode !== undefined && + ts.isExpressionStatement(stmtNode) && + ts.isBinaryExpression(expr) && + (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) { + return lowerDiscardedLogical(lowerer, expr); + } const value = lowerer.lowerExpr(expr); // A bare unit-literal statement (`undefined;`): a pure no-op in JS, // and the IR has no bare-unit VALUE outside a union wrap — drop it @@ -7298,6 +7564,59 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm return { kind: "exprStmt", expr: value, loc: locOf(expr) }; } +/** A discarded `a && b` / `a || b` statement (the minifier's `if`). The + * value form lowers first, exactly as before. Only when it fails on the + * logical's own result type — the operands have no common representation + * (jose's `(!m || m[4] && m[1]) && invalid()` joins a boolean, a string + * and undefined into the checker's `string`) — does the statement lower + * as the control flow it is: `if (a) b;` / `if (!a) b;`, a condition test + * that needs no result value. Diagnostics outside the statement, and + * internal errors, keep the original failure. */ +function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt { + const diagsBefore = lowerer.diags.length; + try { + const value = lowerer.lowerExpr(expr); + if (value.kind === "unitLit") return { kind: "block", body: [], loc: locOf(expr) }; + return { kind: "exprStmt", expr: value, loc: locOf(expr) }; + } catch (e) { + // A probe (a capture buffer is active) keeps its failure: its owner + // picks another lowering, exactly as before. + if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; + const added = lowerer.diags.slice(diagsBefore); + const file = expr.getSourceFile().fileName; + const start = expr.getStart(); + const local = + added.length > 0 && + added.every( + (d) => + d.code !== "SC9001" && + d.loc.file === file && + d.loc.start >= start && + d.loc.end <= expr.end, + ); + if (!local) throw e; + lowerer.diags.length = diagsBefore; + } + const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; + const cond = lowerer.lowerCondition(expr.left); + if (cond.kind === "boolLit") { + return cond.value === and + ? lowerExprStatement(lowerer, expr.right) + : { kind: "block", body: [], loc: locOf(expr) }; + } + const right = (): IrStmt[] => + lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, expr.left, and), () => + and + ? withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, expr.left), () => [ + lowerExprStatement(lowerer, expr.right), + ]) + : [lowerExprStatement(lowerer, expr.right)], + ); + return and + ? { kind: "if", cond, then: right(), else_: [], loc: locOf(expr) } + : { kind: "if", cond, then: [], else_: right(), loc: locOf(expr) }; +} + /** True for a statement-position expression whose evaluation cannot be * observed once the value is discarded: * - a property/element READ (`?.` included) whose receiver IS a @@ -7325,7 +7644,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; @@ -7508,8 +7827,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) { @@ -7565,7 +7884,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)) { @@ -7587,7 +7906,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", @@ -7605,7 +7924,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 @@ -7635,13 +7954,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); @@ -7778,7 +8097,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( @@ -7853,7 +8172,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). @@ -7873,7 +8192,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); @@ -7895,22 +8214,25 @@ function destructuringAssignInto( ), ); } - const packed: IrExpr = { - kind: "recordLit", - fields: remaining.map((f): Extract["fields"][number] => ({ - name: f.name, - value: { - kind: "recordGet", - obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, - shapeId: srcType.shapeId, - field: f.name, - type: f.type, - loc: locOf(prop), - }, - })), - type: { kind: "record", shapeId: restShapeId }, - loc: locOf(prop), - }; + const packed: IrExpr = lowerer.keyOrder.copy( + { + kind: "recordLit", + fields: remaining.map((f): Extract["fields"][number] => ({ + name: f.name, + value: { + kind: "recordGet", + obj: { kind: "varRef", localId: tmp.id, type: srcType, loc: locOf(prop) }, + shapeId: srcType.shapeId, + field: f.name, + type: f.type, + loc: locOf(prop), + }, + })), + type: { kind: "record", shapeId: restShapeId }, + loc: locOf(prop), + }, + srcType.shapeId, + ); if (!ts.isIdentifier(restTo)) { // `({ a, ...box.rest } = src)` — the rest packs identically and // lands through the property/element write machinery. @@ -8219,7 +8541,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({ @@ -8237,13 +8559,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. @@ -8565,7 +8887,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 && @@ -8629,7 +8951,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); @@ -8660,7 +8982,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" && @@ -9173,7 +9495,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 @@ -9208,7 +9530,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { // local and the body opens by ASSIGNING it into the one hoisted // binding — closures capture the shared slot, and the value persists // after the loop (both Node-exact for var). - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, elemValueT); if (varTarget) { const elemType = elemValueT; const loc = locOf(decl.name); @@ -9354,7 +9676,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); @@ -9491,13 +9813,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; } @@ -9529,7 +9851,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); @@ -9543,7 +9865,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({ @@ -9554,7 +9876,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, @@ -9698,7 +10022,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()); @@ -9930,7 +10254,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 }); @@ -9942,7 +10266,7 @@ function lowerForOfDyn( binds, ); } else if (ts.isIdentifier(decl.name)) { - const varTarget = forOfVarTarget(lowerer, decl); + const varTarget = forOfVarTarget(lowerer, decl, DYN); if (varTarget) { // `for (var x of d)`: the element mechanics stay on a hidden // per-iteration local; the body opens by assigning the ONE @@ -10113,7 +10437,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 { @@ -10479,7 +10803,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; @@ -10592,7 +10916,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 || @@ -10703,7 +11027,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 }; @@ -10786,3 +11110,25 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt lowerer.scopes.pop(); } } + +/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias + * of the global Buffer is the same alias plumbing as the const spelling + * when nothing reassigns it and no code that runs before the declaration + * can read it (the require-binding early-reference analysis), so the + * hoisted undefined is unobservable. */ +export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + if ( + !ts.isIdentifier(decl.name) || + !ts.isVariableDeclarationList(decl.parent) || + (decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 || + decl.initializer === undefined || + !bufferNamespaceMember(lowerer, decl.initializer) + ) + return false; + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + return ( + symbol !== undefined && + bindingNeverReassigned(lowerer, symbol, decl) && + requireEarlyReference(lowerer.program, decl) === null + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index 5f3d56114..5769e1e0a 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 @@ -492,7 +492,7 @@ function followOptionsArg( } catch { return node; } - const decl = sym ? lowerer.checker.valueDeclarationOf(sym) : undefined; + const decl = sym ? sym.valueDeclaration : undefined; if (!sym || !decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return node; if ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0) return node; @@ -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..32870c2cf 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 symbol.declarations) { + 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 @@ -561,7 +728,7 @@ export function lowerTestDirectCall( if (!ts.isIdentifier(callee)) return null; if (lowerer.builtinNamespaceModuleOf(callee) !== "test") return null; const calleeSym = lowerer.checker.getSymbolAtLocation(callee); - const decl = calleeSym ? lowerer.checker.declarationsOf(calleeSym)[0] : undefined; + const decl = calleeSym ? calleeSym.declarations[0] : undefined; if (decl && ts.isNamespaceImport(decl)) { lowerer.unsupported( "SC1013", @@ -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 c2a54dfc0..22983fbb4 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -284,14 +284,14 @@ export const TIMER_MODULE_MEMBERS: ReadonlySet = new Set([ * path. Null when the argument might be a real handle. */ function tolerantClearNoop(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr | null { const noop: IrExpr = { kind: "libCall", fn: "timers.clearNoop", args: [], type: VOID, loc }; - if (expr.arguments.length === 0) return noop; - if (expr.arguments.length !== 1) return null; - let arg = expr.arguments[0]!; + if (expr.arguments.nodes.length === 0) return noop; + if (expr.arguments.nodes.length !== 1) return null; + let arg = expr.arguments.nodes[0]!; // `{} as never` / parenthesized spellings: the cast changes no value. while (ts.isAsExpression(arg) || ts.isTypeAssertion(arg) || ts.isParenthesizedExpression(arg)) arg = arg.expression; if (arg.kind === ts.SyntaxKind.NullKeyword) return noop; - if (ts.isObjectLiteralExpression(arg) && arg.properties.length === 0) return noop; + if (ts.isObjectLiteralExpression(arg) && arg.properties.nodes.length === 0) return noop; if (ts.isIdentifier(arg)) { const t = lowerer.mapTypeOf(lowerer.typeOf(arg)); if (t && (t.kind === "nullT" || t.kind === "undefinedT")) return noop; @@ -330,7 +330,7 @@ function isRealTimerCallee(lowerer: Lowerer, callee: ts.Expression): boolean { if (ts.isIdentifier(callee)) return lowerer.builtinImportOf(callee)?.module === "timers"; if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) { const sym = lowerer.checker.getSymbolAtLocation(callee.expression); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; return decl !== undefined && ts.isNamespaceImport(decl); } return false; @@ -369,7 +369,7 @@ function lowerTimerCall( // Delayed timers share callback adaptation and the 1ms default. Trailing // arguments are captured now and delivered when the callback fires. if (member === "setTimeout" || member === "setInterval") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( `${member} with 0 arguments`, expr, @@ -377,19 +377,25 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 2 - ? makeTimerArgsThunk(lowerer, expr.arguments[0]!, expr.arguments.slice(2), member, loc) + expr.arguments.nodes.length > 2 + ? makeTimerArgsThunk( + lowerer, + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(2), + member, + loc, + ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); // An omitted delay is NaN, Node's undefined: the loop clamps it to // 1ms, and mock.timers queues it to run at the next tick. const ms: IrExpr = - expr.arguments.length >= 2 - ? lowerer.lowerExpr(expr.arguments[1]!) + expr.arguments.nodes.length >= 2 + ? lowerer.lowerExpr(expr.arguments.nodes[1]!) : { kind: "numLit", value: NaN, type: F64, loc }; if (member === "setInterval") { return { kind: "libCall", fn: "timers.setInterval", args: [cb, ms], type: F64, loc }; @@ -410,10 +416,10 @@ function lowerTimerCall( if (member === "clearTimeout") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearTimeout with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr); } - let handle = lowerer.lowerExpr(expr.arguments[0]!); + let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!); if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); const ref = varRef(stored.id, DYN, loc); @@ -423,7 +429,7 @@ function lowerTimerCall( result: { kind: "ternary", cond: { kind: "dynTest", test: "number", value: ref, type: BOOL, loc }, - then: lowerer.coerceInto(expr.arguments[0]!, ref, F64), + then: lowerer.coerceInto(expr.arguments.nodes[0]!, ref, F64), else_: numLit(0, loc), type: F64, loc, @@ -436,7 +442,7 @@ function lowerTimerCall( if (handle.type.kind !== "f64") { lowerer.noLowering( `clearTimeout of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setTimeout returned, or undefined or null", ); } @@ -445,14 +451,14 @@ function lowerTimerCall( if (member === "clearInterval") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearInterval with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearInterval with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearInterval of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Timeout setInterval returned, or undefined or null", ); } @@ -462,7 +468,7 @@ function lowerTimerCall( // the f64 immediate id (its own space — clearTimeout of an Immediate // no-ops, like Node); the callback adapts like setTimeout's. if (member === "setImmediate") { - if (expr.arguments.length === 0) { + if (expr.arguments.nodes.length === 0) { lowerer.noLowering( "setImmediate with 0 arguments", expr, @@ -470,18 +476,18 @@ function lowerTimerCall( ); } const cb = - expr.arguments.length > 1 + expr.arguments.nodes.length > 1 ? makeTimerArgsThunk( lowerer, - expr.arguments[0]!, - expr.arguments.slice(1), + expr.arguments.nodes[0]!, + expr.arguments.nodes.slice(1), "setImmediate", loc, ) : adaptZeroArgTimerCallback( lowerer, - lowerer.lowerExpr(expr.arguments[0]!), - expr.arguments[0]!, + lowerer.lowerExpr(expr.arguments.nodes[0]!), + expr.arguments.nodes[0]!, loc, ); return { kind: "libCall", fn: "timers.setImmediate", args: [cb], type: F64, loc }; @@ -489,14 +495,14 @@ function lowerTimerCall( if (member === "clearImmediate") { const noop = tolerantClearNoop(lowerer, expr, loc); if (noop) return noop; - if (expr.arguments.length !== 1) { - lowerer.noLowering(`clearImmediate with ${expr.arguments.length} arguments`, expr); + if (expr.arguments.nodes.length !== 1) { + lowerer.noLowering(`clearImmediate with ${expr.arguments.nodes.length} arguments`, expr); } - const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments[0]!), loc); + const handle = optionalTimerHandle(lowerer, lowerer.lowerExpr(expr.arguments.nodes[0]!), loc); if (handle.type.kind !== "f64") { lowerer.noLowering( `clearImmediate of '${lowerer.fmt(handle.type)}' handles`, - expr.arguments[0]!, + expr.arguments.nodes[0]!, "the handle is the Immediate setImmediate returned, or undefined or null", ); } diff --git a/packages/compiler/src/frontend/lowering/lower-url.ts b/packages/compiler/src/frontend/lowering/lower-url.ts index f12d6b267..ee7e4a0af 100644 --- a/packages/compiler/src/frontend/lowering/lower-url.ts +++ b/packages/compiler/src/frontend/lowering/lower-url.ts @@ -35,7 +35,7 @@ function lowerUrlOperation( expr: ts.NewExpression | ts.CallExpression, operation: "new" | "canParse" | "parse", ): IrExpr { - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const loc = locOf(expr); const resultType = operation === "new" ? URL_T : operation === "canParse" ? BOOL : DYN; const checkedResult = (value: IrExpr): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 093c78632..bacdc7941 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -241,25 +241,26 @@ test("optional union widening keeps refused and missing contracts current", () = }); test("stdlib provenance is cached by symbol identity, including merged and shadowed declarations", () => { - const builtin = {} as Symbol, - shadow = {} as Symbol; const userFile = { fileName: "user.ts" } as SourceFile; const library = { fileName: "library.d.ts" } as SourceFile; const userDeclaration = { getSourceFile: () => userFile } as Node; const libraryDeclaration = { getSourceFile: () => library } as Node; - const declarationsOf = vi.fn((symbol: Symbol) => - symbol === builtin ? [userDeclaration, libraryDeclaration] : [userDeclaration], - ); - const lowerer = new Lowerer( - { getTypeChecker: () => ({ declarationsOf }) } as unknown as Program, - userFile, - [], - ); + let declarationReads = 0; + const symbolWith = (declarations: readonly Node[]): Symbol => + ({ + get declarations() { + declarationReads++; + return declarations; + }, + }) as unknown as Symbol; + const builtin = symbolWith([userDeclaration, libraryDeclaration]), + shadow = symbolWith([userDeclaration]); + const lowerer = new Lowerer({ getTypeChecker: () => ({}) } as unknown as Program, userFile, []); vi.spyOn(lowerer, "isStdlibFile").mockImplementation((file) => file === library); for (let i = 0; i < 20; i++) { expect(lowerer.isStdlibSymbol(builtin)).toBe(true); expect(lowerer.isStdlibSymbol(shadow)).toBe(false); expect(lowerer.isStdlibSymbol(undefined)).toBe(false); } - expect(declarationsOf).toHaveBeenCalledTimes(2); + expect(declarationReads).toBe(2); }); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a77142cb0..39cb565d5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -20,6 +20,7 @@ import { } from "./require-initialization.js"; import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; +import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js"; import { checkedClassAssertion } from "./class-assertions.js"; import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js"; import { isOptionalProcessStreamProperty } from "./builtins/process.js"; @@ -31,6 +32,7 @@ import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; @@ -72,6 +74,7 @@ import { uninhabitedArmRetagHelper, strandedUnitTrap, strandedCoercionTrap, + recordUnionRouteHelper, recordUnionWrapHelper, unionRetagHelper, narrowedArmHelper, @@ -269,7 +272,11 @@ import { stdlibGlobalMember, nodeTypesOnlySymbol, } from "./surfaces.js"; -import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js"; +import { + type BuiltinClassRef, + builtinClassAliasOf, + runtimeClassValueOfType, +} from "./builtins/class-values.js"; import { type FileParts, splitFiles, @@ -302,6 +309,8 @@ import { type ClassIteratorInfo, type GenericClassInfo, registerBuiltinErrorClasses, + defineClassValueConstruct, + runtimeClassConstructs, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, @@ -468,6 +477,8 @@ import { lowerConsoleNew } from "./lower-console.js"; import { lowerNumberStaticCall, lowerNumberStaticProperty } from "./builtins/numbers.js"; import { dateJsonType, isDateJsonSource, lowerDateCall } from "./builtins/dates.js"; import { + fsConstantMemberRead, + fsConstantsObject, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, @@ -671,7 +682,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; @@ -688,7 +699,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) || @@ -845,7 +856,25 @@ type RuntimeFenceTarget = /** The Lowerer's pass configuration (see lowerToIr). */ export interface LowererMode { + /** Symbols whose deferred diagnostics reachable emit already flushed — + * the remainder must not report them a second time. */ + alreadyFlushed?: ReadonlySet; + /** LowerOptions.externalTypes, threaded through every lowering pass. */ + externalTypes?: ReadonlyMap; + /** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */ + externalTypeSpecifiersByFile?: ReadonlyMap; + /** Program-validated ambient declaration symbols for each FFI name. + * Undefined in discovery's legacy call-local validation path. */ + ffiBindingSymbols?: ReadonlyMap>; + /** The build's outbound native FFI declarations. */ + ffiImports?: readonly IrFfiImport[]; + /** Statically resolved child_process.fork program roots, in stable target-id order. */ + forkTargets?: readonly ts.SourceFile[]; frontendServices?: FrontendServices | undefined; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; + /** LowerOptions.libraryCallbacks (see there). */ + libraryCallbacks?: boolean; projectCheckJs?: boolean; /** Names of bodies a prior reachability pass reached; null lowers everything. */ reachable?: ReadonlySet | null; @@ -853,35 +882,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( @@ -1179,7 +1190,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}`; @@ -1187,9 +1198,9 @@ export function staticImportNamespaceType( const symbol = lowerer.checker.getSymbolAtLocation(arg); const source = symbol && - lowerer.checker - .declarationsOf(symbol) - .find((decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile); + symbol.declarations.find( + (decl): decl is ts.SourceFile => ts.isSourceFile(decl) && !decl.isDeclarationFile, + ); if ( source && !source.fileName.endsWith(".cts") && @@ -1444,7 +1455,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir // slot. JS inference reports the body type (`boolean` for // `enabled = true`) rather than that call-site type, so recover the // syntactic optionality from the parameter declaration. - const decl = lowerer.checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; mapped = jsOpenObjectType(decl, mapped, lowerer.shapes, lowerer.unions); const optional = decl !== undefined && @@ -1456,7 +1467,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir return mapped; }); const retT = lowerer.checker.getReturnTypeOfSignature(sig); - const sigDecl = lowerer.checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -1485,7 +1496,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { lowerer.checker.getPropertiesOfType(t).length !== 0 ) return null; - const decl = lowerer.checker.signatureDeclaration(sigs[0]!); + const decl = sigs[0]!.declaration; if ( decl === undefined || !( @@ -1496,15 +1507,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) @@ -1531,16 +1542,16 @@ function pureSingleCallSignatureOf(lowerer: Lowerer, t: ts.Type): ts.Signature | lowerer.checker.getConstructSignatures(t).length === 0 && sigs[0]!.getTypeParameters().length === 0 && sigs[0]!.getParameters().every((p) => { - const pDecl = lowerer.checker.valueDeclarationOf(p); + const pDecl = p.valueDeclaration; return !pDecl || !ts.isParameter(pDecl) || pDecl.dotDotDotToken === undefined; }) && // A SYNTHESIZED rest param (tsc's `arguments` inference — no // valueDeclaration to carry the dotDotDot): param-count mismatch // against the signature's declaration; the whole value stays dyn. (() => { - const sigDecl = lowerer.checker.signatureDeclaration(sigs[0]!); + const sigDecl = sigs[0]!.declaration; 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 @@ -1594,7 +1605,7 @@ export function jsFuncNameOf(node: ts.Node, lowerer?: Lowerer): string | null { const symbol = lowerer.resolveValueSymbol(n); if (!symbol || seen.has(symbol)) break; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isFunctionDeclaration(declaration)) { n = declaration; break; @@ -1712,7 +1723,7 @@ export class Lowerer { get stringIndexBounds(): StringIndexBounds { this.stringIndexBoundsCache ??= new StringIndexBounds( (node) => this.checker.getSymbolAtLocation(node) ?? null, - (symbol) => this.checker.valueDeclarationOf(symbol), + (symbol) => symbol.valueDeclaration, (node) => (this.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0, ); return this.stringIndexBoundsCache; @@ -2074,7 +2085,7 @@ export class Lowerer { // directly from the aliased declaration as a final read-side hook, so // the initializer cannot fall back to a narrowing adapter. if (ts.isIdentifier(node) && fallback?.kind === "func") { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( decl && ts.isVariableDeclaration(decl) && @@ -2433,6 +2444,9 @@ export class Lowerer { * Reachability lowers inits before the declarations they discover, so * these rebuild after the worklist restores historical shape metadata. */ readonly shapeOrderHelperFinalizers: (() => void)[] = []; + /** Construction witnesses that choose each shape's enumeration order + * once lowering settles (key-order-witness.ts). */ + readonly keyOrder = new KeyOrderNotes(); /** Emit-override specializations (`%C.emit:` — lower-event-emitter.ts's * emit-overrides block): interned names, the drive-loop queue, and the * currently-lowering specialization's context (the super-forward @@ -2444,6 +2458,9 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** structuralNarrowing's assignable classes, keyed by static and + * narrowed class name. */ + readonly structuralClassPeers = new Map(); /** Interned node:assert helpers (deep-equality comparisons keyed by * typeKey, throws wrappers keyed by callback type + expected class) — @@ -2774,6 +2791,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; @@ -2968,28 +2988,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(); @@ -3003,6 +3024,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 } @@ -3018,10 +3040,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)) @@ -3060,7 +3082,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; @@ -3176,7 +3198,7 @@ export class Lowerer { let sym = type.getSymbol(); if (!sym) return null; if (sym.flags & ts.SymbolFlags.Alias) sym = this.checker.getAliasedSymbol(sym); - for (const decl of this.checker.declarationsOf(sym)) { + for (const decl of sym.declarations) { if ( ts.isSourceFile(decl) && !decl.isDeclarationFile && @@ -3216,6 +3238,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}`; @@ -3390,7 +3416,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) @@ -3400,9 +3426,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); @@ -3436,7 +3462,7 @@ export class Lowerer { * every project-module spelling. */ private moduleSourceFileOf(from: ts.SourceFile, spec: ts.StringLiteral): ts.SourceFile | null { const moduleSymbol = this.checker.getSymbolAtLocation(spec); - for (const decl of moduleSymbol ? this.checker.declarationsOf(moduleSymbol) : []) { + for (const decl of moduleSymbol ? moduleSymbol.declarations : []) { if (ts.isSourceFile(decl)) return decl; } return resolveImport(this.program, from, spec.text); @@ -3481,9 +3507,7 @@ export class Lowerer { } if (!ts.isIdentifier(value)) return null; const symbol = this.checker.getSymbolAtLocation(value); - const namespaceDecl = symbol - ? this.checker.declarationsOf(symbol).find(ts.isNamespaceImport) - : undefined; + const namespaceDecl = symbol ? symbol.declarations.find(ts.isNamespaceImport) : undefined; if (namespaceDecl === undefined) return null; const importDecl = namespaceDecl.parent?.parent; if (!ts.isImportDeclaration(importDecl) || !ts.isStringLiteral(importDecl.moduleSpecifier)) @@ -3503,7 +3527,7 @@ export class Lowerer { ): string | null { if (symbol === undefined || seenSymbols.has(symbol)) return null; seenSymbols.add(symbol); - const declarations = this.checker.declarationsOf(symbol); + const declarations = symbol.declarations; // Route-aware alias hops run before declaration-file ownership. An // exact import must keep the specifier it actually named, rather than @@ -3572,9 +3596,9 @@ export class Lowerer { let sym: ts.Symbol | undefined = alias; for (let hop = 0; sym !== undefined && hop < 32; hop++) { if (hop > 0 && sym.flags & ts.SymbolFlags.Alias && this.globalsBySymbol.has(sym)) return sym; - const d = this.checker - .declarationsOf(sym) - .find((x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x)); + const d = sym.declarations.find( + (x) => ts.isImportClause(x) || ts.isImportSpecifier(x) || ts.isExportSpecifier(x), + ); let spec: ts.Expression | undefined; let name: string | undefined; if (d && ts.isImportClause(d) && ts.isImportDeclaration(d.parent)) { @@ -3621,7 +3645,7 @@ export class Lowerer { * see resolveValueSymbol. Null when `symbol` is not such a property (or * the property's value is not a plain identifier reference). */ private cjsExportValueSymbol(symbol: ts.Symbol): ts.Symbol | null { - const d = this.checker.declarationsOf(symbol)[0]; + const d = symbol.declarations[0]; if (!d) return null; // MEMBER-form class exports (`exports.C = C` — commander's error.js): // alias plumbing exactly like a table entry, so importers land on the @@ -3667,7 +3691,7 @@ export class Lowerer { let value = this.checker.getSymbolAtLocation(assign.right); if (!value) return null; if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value); - const isClass = this.checker.declarationsOf(value).some((decl) => ts.isClassDeclaration(decl)); + const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl)); return isClass ? value : null; } @@ -3713,7 +3737,7 @@ export class Lowerer { * member-symbol fallback and namespace classification in lockstep. */ private localModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { const sym = this.checker.getSymbolAtLocation(expr); - const decls = sym ? this.checker.declarationsOf(sym) : []; + const decls = sym ? sym.declarations : []; const decl = decls.find(ts.isImportClause) ?? decls[0]; if (!decl) return null; if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); @@ -3741,31 +3765,27 @@ export class Lowerer { * static storage or callable/class metadata. */ private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const decl = binding - ? this.checker.declarationsOf(binding).find(ts.isVariableDeclaration) - : undefined; + const decl = binding ? binding.declarations.find(ts.isVariableDeclaration) : undefined; if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null; const target = createRequireProgramModuleOf(this, decl.initializer); if (target === null) return null; const moduleSym = this.checker.getSymbolAtLocation(target.dep); let symbol = moduleSym?.getExports().get("export=" as ts.__String); if (!symbol) return null; - const wholeAssignment = this.checker - .declarationsOf(symbol) - .find((d): d is ts.BinaryExpression => { - if ( - !ts.isBinaryExpression(d) || - !ts.isExpressionStatement(d.parent) || - !ts.isSourceFile(d.parent.parent) - ) - return false; - const exported = cjsExportAssignmentOf(d.parent); - return ( - exported?.kind === "table" && - exported.obj === null && - cjsExportDiscardReason(d.parent) === null - ); - }); + const wholeAssignment = symbol.declarations.find((d): d is ts.BinaryExpression => { + if ( + !ts.isBinaryExpression(d) || + !ts.isExpressionStatement(d.parent) || + !ts.isSourceFile(d.parent.parent) + ) + return false; + const exported = cjsExportAssignmentOf(d.parent); + return ( + exported?.kind === "table" && + exported.obj === null && + cjsExportDiscardReason(d.parent) === null + ); + }); if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; @@ -3792,9 +3812,7 @@ export class Lowerer { * target module's real export symbol. */ private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const element = binding - ? this.checker.declarationsOf(binding).find(ts.isBindingElement) - : undefined; + const element = binding ? binding.declarations.find(ts.isBindingElement) : undefined; if ( !element || element.name === undefined || @@ -3842,7 +3860,7 @@ export class Lowerer { if (sym && sym.flags & ts.SymbolFlags.Alias) { // `module.exports = TypeError`: the binding IS the builtin global. if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; - const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; + const d = this.checker.getAliasedSymbol(sym).declarations[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { let init = d.initializer; @@ -3907,7 +3925,7 @@ export class Lowerer { if (g.type.kind === "func") g.type = this.runtimeOptionalBindingType(ident, g.type); return g; } - for (const d of this.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const byDecl = this.globalsByDeclNode.get(d); if (byDecl) return byDecl; } @@ -4098,7 +4116,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)) && @@ -4244,7 +4262,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) @@ -4270,9 +4288,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); @@ -4414,7 +4432,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 => { @@ -4423,7 +4441,7 @@ export class Lowerer { }; const recordFlows = new RecordFlowClasses(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, mayBeRecord: (node) => { if (node.parent && ts.isParameter(node.parent)) return plainRecordShape(parameterType(node)) !== null; @@ -4454,7 +4472,7 @@ export class Lowerer { const nanCoded = new NanCodedReads( { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, bytesElemOf: (receiver) => { const t = mappedOrNull(receiver); return t?.kind === "bytes" ? t.elem : null; @@ -4506,7 +4524,7 @@ export class Lowerer { const seen = new Set(); while (symbol && !signatureBySymbol.has(symbol) && !seen.has(symbol)) { seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isMethodDeclaration(declaration)) { symbol = symbolOf(declaration.name); } else if ( @@ -4623,12 +4641,12 @@ export class Lowerer { if (isJsSourceFile(sf) || sf.isDeclarationFile) continue; ts.walkPreorder(sf, (node) => { if (!ts.isClassDeclaration(node)) return undefined; - const genericClass = (node.typeParameters?.length ?? 0) > 0; - for (const member of node.members) + const genericClass = (node.typeParameters?.nodes.length ?? 0) > 0; + for (const member of node.members.nodes) if ( ts.isMethodDeclaration(member) && member.body && - (genericClass || (member.typeParameters?.length ?? 0) > 0) + (genericClass || (member.typeParameters?.nodes.length ?? 0) > 0) ) genericClassMethods.push(member); return undefined; @@ -4651,7 +4669,7 @@ export class Lowerer { 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; + const decl = symbol ? symbol.valueDeclaration : undefined; return ( symbol !== null && !reassigned.has(symbol) && @@ -4692,7 +4710,7 @@ export class Lowerer { const read = peel(node); if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false; const source = symbolOf(read.expression); - const declaration = source ? this.checker.valueDeclarationOf(source) : undefined; + const declaration = source ? source.valueDeclaration : undefined; if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) return false; const init = peel(declaration.initializer); @@ -4703,7 +4721,7 @@ export class Lowerer { !this.isStdlibGlobal(init.expression.expression, "Object") ) return false; - const object = init.arguments[0]; + const object = init.arguments.nodes[0]; const objectType = object ? this.mapTypeOf(this.typeOf(object)) : null; return objectType?.kind === "dyn"; }; @@ -4788,7 +4806,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") @@ -4921,7 +4939,7 @@ export class Lowerer { * exactly `T | undefined`: an assignment from a missing read stores the * undefined arm while the checker narrows later reads to `T`. */ const optionalInstanceField = (symbol: ts.Symbol): boolean => { - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false; if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false; const sf = decl.getSourceFile(); @@ -4956,7 +4974,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))) @@ -4991,7 +5009,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); @@ -5005,13 +5023,42 @@ export class Lowerer { return out; }; const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => { - if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init); + if (ts.isObjectBindingPattern(name)) { + // `const { 0: a, 1: b, length } = parts` reads positions like the + // equivalent array pattern: each may sit past the end. + // Regex match results keep their own pattern lowering. + const arrayType = this.mapTypeOf(this.typeOf(init)); + const initType = this.checker.getNonNullableType(this.typeOf(init)); + const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); + const matchResult = + initSymbol !== undefined && + (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); + 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) + ); + }); + return scanPositions(positions, init, arrayType); + } if (!ts.isArrayBindingPattern(name)) return false; const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; + return scanPositions( + name.elements.nodes.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), + init, + sourceType, + ); + }; + const scanPositions = ( + elements: readonly ts.BindingElement[], + init: ts.Expression, + sourceType: IrType & { kind: "array" }, + ): boolean => { let changed = false; - name.elements.forEach((el) => { - if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; + elements.forEach((el) => { + if (el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); if (symbol && !el.initializer && !optionalSymbols.has(symbol)) { @@ -5051,7 +5098,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)) @@ -5196,7 +5243,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); @@ -5260,7 +5307,7 @@ export class Lowerer { // monomorphic ABI or poisons valid generic value programs. if (this.genericFnsBySymbol.has(symbol)) return false; seen.add(symbol); - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) { if (promoteHofCallback(peel(declaration.initializer), parameterIndices, seen)) changed = true; @@ -5310,7 +5357,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; @@ -5332,7 +5381,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; @@ -5368,7 +5417,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; @@ -5392,7 +5441,7 @@ export class Lowerer { seen.add(symbol); const fn = functionDeclBySymbol.get(symbol); if (fn) return bodyReturnsOptional(fn); - const decl = this.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; return ( !!decl && ts.isVariableDeclaration(decl) && @@ -5473,7 +5522,7 @@ export class Lowerer { }; const ownership = new ArrayOwnership(sourceFiles, { symbolOf, - declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol), + declarationOf: (symbol) => symbol.valueDeclaration, typeOf: typeOrNull, instanceAssignableTo: (owner, target) => panicSafe( @@ -5498,7 +5547,7 @@ export class Lowerer { // flow type such as `Array.isArray(x)`'s `any[]` only narrows it. const e = peel(node); const symbol = ts.isIdentifier(e) ? symbolOf(e) : null; - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const declared = declaration && (ts.isVariableDeclaration(declaration) || ts.isParameter(declaration)) && @@ -5550,12 +5599,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) && @@ -5781,7 +5830,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; @@ -5820,7 +5869,7 @@ export class Lowerer { const assertion = forwardedAssertion(node.right); const symbol = assertion ? symbolOf(node.left) : null; // Only a variable's storage is promoted (see the final pass). - const declaration = symbol ? this.checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; if ( assertion && symbol && @@ -5877,9 +5926,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); @@ -5906,9 +5955,7 @@ export class Lowerer { let nativeArrayReceiver = receiver?.kind === "array" || tuple; if (nativeArrayReceiver && ts.isIdentifier(receiverNode)) { const receiverSymbol = symbolOf(receiverNode); - const declaration = receiverSymbol - ? this.checker.valueDeclarationOf(receiverSymbol) - : undefined; + const declaration = receiverSymbol ? receiverSymbol.valueDeclaration : undefined; if (declaration && ts.isVariableDeclaration(declaration)) { const declared = this.mapTypeOf(this.typeOf(declaration.name)); if ( @@ -5942,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; @@ -5968,7 +6015,7 @@ export class Lowerer { // number) => ...`): a possibly missing argument widens that // parameter, as for a callback the same value is passed as. if (!ts.isIdentifier(node.expression) || reassigned.has(symbol)) return; - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const value = declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? peel(declaration.initializer) @@ -5976,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 || @@ -5984,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] : [], @@ -6004,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 @@ -6019,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)) { @@ -6039,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; @@ -6059,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); @@ -6108,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); @@ -6133,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, @@ -6195,7 +6242,7 @@ export class Lowerer { // there, whatever its declared type. for (const symbol of earlyReadVarSymbols(this, sourceFiles)) { // Text codec bindings keep their own presence-checked storage. - const declaration = this.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; const mapped = declaration && ts.isVariableDeclaration(declaration) ? mappedOrNull(declaration.name) @@ -6343,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); } @@ -6595,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; @@ -6743,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 @@ -7009,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] : [], + ), ), ), ); @@ -7049,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), ); @@ -7409,6 +7459,13 @@ export class Lowerer { this.settleGenericDemandPriorities(); this.shapes.settleDeclaredOrderPriorities(); for (const finalize of this.shapeOrderMetadataFinalizers) finalize(); + applyKeyOrderWitnesses(this, [ + ...orderedUnits, + ...initFunctions, + ...instanceFunctions, + ...this.liftedFns, + ...this.implicitFns, + ]); for (const finalize of this.shapeOrderHelperFinalizers) finalize(); const functions = [ ...orderedUnits, @@ -7418,6 +7475,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. @@ -7730,13 +7788,11 @@ export class Lowerer { const ownSym = widened.getSymbol(); if ( ownSym && - this.checker - .declarationsOf(ownSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ) + ownSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ) ) { this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); throw new PoisonError(); @@ -7778,13 +7834,11 @@ export class Lowerer { const stdlibOwnSym = widened.getSymbol(); const stdlibNominal = stdlibOwnSym !== undefined && - this.checker - .declarationsOf(stdlibOwnSym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), - ); + stdlibOwnSym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + this.isStdlibFile(d.getSourceFile()), + ); // OVERLOADED call signatures (SC2007) — after the generic branch, so a // generic overload set keeps the monomorphization story: a value of a // multi-signature type has no single compiled signature to hold. @@ -7880,7 +7934,11 @@ export class Lowerer { access: ts.PropertyAccessExpression | ts.ElementAccessExpression, use: "read" | "call", ): IrExpr | null { - return fenceStaticResponseMember(this, access, use) ?? fenceStaticBlobMember(this, access, use); + return ( + fenceStaticResponseMember(this, access, use) ?? + fenceStaticBlobMember(this, access, use) ?? + fenceWebCryptoMember(this, access, use) + ); } fenceUnsupportedFetchConstructorMember( @@ -7997,7 +8055,7 @@ export class Lowerer { // JSDoc and initializer inference are declaration types, not flow // proofs. A checked specialization must not reinstate those // stale layouts (mutable/null slots and heterogeneous JS arrays). - const declaration = sym && this.checker.valueDeclarationOf(sym); + const declaration = sym && sym.valueDeclaration; const declared = declaration && ts.isParameter(declaration) ? this.mapTypeOf(this.checker.getTypeAtLocation(declaration.name)) @@ -8551,6 +8609,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 @@ -8770,6 +8840,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 }; } } @@ -8890,6 +8961,7 @@ export class Lowerer { }); if (baseTag >= 0) { if (!sub || !this.ctorAbiEquals(sub, c)) break; + defineClassValueConstruct(this, c); const widened: IrExpr = { kind: "upcast", value: expr, @@ -9110,6 +9182,14 @@ export class Lowerer { return recordUnionWrapHelper(this, source, toId, loc); } + recordUnionRouteHelper( + source: IrType & { kind: "record" }, + toId: string, + loc: SrcLoc, + ): string | null { + return recordUnionRouteHelper(this, source, toId, loc); + } + unionRetagHelper( fromId: string, toId: string, @@ -9306,7 +9386,7 @@ export class Lowerer { if (!ts.isIdentifier(n)) return null; const sym = this.resolveValueSymbol(n); if (!sym) return null; - const decls = this.checker.declarationsOf(sym); + const decls = sym.declarations; if (decls.length === 0) return null; const units = new Set(); let allConst = true; @@ -9513,13 +9593,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), }; @@ -9565,7 +9645,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 && @@ -9577,20 +9657,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" && @@ -9610,16 +9690,23 @@ export class Lowerer { } } } - let e = this.lowerExpr(node); + const e = this.lowerExpr(node); + return expected ? this.coerceExistingInto(node, e, expected) : e; + } + + /** The conversion lowerExprExpecting applies to `e`, the lowered value + * of `node` when it is not fresh construction the destination directs + * (a literal, conditional, or empty collection). */ + coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr { if ( - (expected?.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || - (expected?.kind === "dyn" && isJsSourceFile(node.getSourceFile())) + (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || + (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile())) ) { // A destination that accepts undefined must retain an unchecked // read's storage value even when the checker still calls it present. e = this.runtimeOptionalSourceValue(node, e) ?? e; } - return expected ? this.coerceInto(node, e, expected) : e; + return this.coerceInto(node, e, expected); } /** An unannotated empty collection has no existing elements or aliases @@ -9630,8 +9717,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); @@ -10079,7 +10166,7 @@ export class Lowerer { // declaration poisons under a source-anchored diagnostic, deferred // like any collection fence — never a crashed CLI. if (isCheckerPanic(e)) { - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; sink.push( checkerPanicDiag( e.message.split("\n", 1)[0]!, @@ -10205,7 +10292,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) || @@ -10213,18 +10300,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; @@ -10246,7 +10333,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 @@ -10497,7 +10584,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 @@ -10573,6 +10660,10 @@ export class Lowerer { return lowerDefaultExport(this, stmt); } + /** Statements %main runs before any module initializer: runtime + * registrations the lowering passes add. */ + readonly mainPrelude: IrStmt[] = []; + buildMain(): IrFunction { return buildMain(this); } @@ -10725,7 +10816,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", @@ -11000,12 +11091,12 @@ export class Lowerer { // symbol — which is what declareParams registered the local under. // On a miss, normalize to the declaration's key and retry (ordinary // bindings never reach this — their two sides intern to one symbol). - const vd = this.checker.valueDeclarationOf(symbol); + const vd = symbol.valueDeclaration; if ( 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 || @@ -11051,9 +11142,7 @@ export class Lowerer { }); if (resolved.error !== null) { const location = - blame ?? - (symbol === undefined ? undefined : this.checker.declarationsOf(symbol)[0]) ?? - this.entry; + blame ?? (symbol === undefined ? undefined : symbol.declarations[0]) ?? this.entry; const message = `the binding '${resolved.origin!.name}' captured through a plain nested function (the declaration has no static storage a capture can thread — bind the value through a typed const, or read it in the declaring scope)`; this.unsupported("SC1090", location, message); } @@ -11135,7 +11224,7 @@ export class Lowerer { // concrete signature — name the shape instead of the generic // binding-form text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if (d && ts.isVariableDeclaration(d) && d.initializer === undefined) { const t = this.checker.getTypeOfSymbol(symbol); const parts = t.isUnionType() ? ts.constituentTypes(t) : [t]; @@ -11157,7 +11246,7 @@ export class Lowerer { // assignment are `undefined`, and this binding's type has no slot for // that value — name the shape instead of the generic no-lowering text. if (symbol) { - const d = this.checker.valueDeclarationOf(symbol); + const d = symbol.valueDeclaration; if ( d && ts.isVariableDeclaration(d) && @@ -11681,6 +11770,7 @@ export class Lowerer { lowerStaticReadableStreamReaderCall(this, expr) ?? lowerStaticResponseCall(this, expr) ?? lowerStaticBlobCall(this, expr) ?? + lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? @@ -12018,7 +12108,7 @@ export class Lowerer { } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The CommonJS twin: `const fs = require("fs")` binds the same // namespace surface as `import * as fs from "node:fs"` — and the @@ -12083,7 +12173,7 @@ export class Lowerer { } if (ts.isIdentifier(expr)) { const symbol = this.checker.getSymbolAtLocation(expr); - const decl = symbol ? this.checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (!decl) return null; // The DEFAULT-import twin: Node's default export of a CJS builtin // IS the module object, so `import path from "node:path"` exposes @@ -12216,13 +12306,7 @@ export class Lowerer { COPYFILE_FICLONE_FORCE: 4, }; const value = own(MODES, expr.name.text); - if (value === undefined) { - this.noLowering( - `fs.constants.${expr.name.text}`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + if (value === undefined) return fsConstantMemberRead(expr); return { kind: "numLit", value, type: F64, loc }; } } @@ -12291,14 +12375,8 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } - if (bi.member === "constants" && bi.module === "fs") { - // A bare `fs.constants` read (not one of the baked bits above). - this.noLowering( - `fs.constants`, - expr, - "F_OK, R_OK, W_OK, X_OK, and COPYFILE_* are the lowered constants", - ); - } + // A bare `fs.constants` read (not one of the baked bits above). + if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc); if (builtinModuleFnOf(this, bi.module, bi.member)) { this.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts index 368ce3c5f..d2425750b 100644 --- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts +++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts @@ -31,7 +31,7 @@ export function lowerModuleNamespaceElement( ? lowerer.checker.getAliasedSymbol(exported) : exported; if (!(target.flags & ts.SymbolFlags.Value)) return; - const declaration = lowerer.checker.valueDeclarationOf(target); + const declaration = target.valueDeclaration; const identifier = declaration?.name as ts.Node | undefined; if (!identifier || !ts.isIdentifier(identifier)) { lowerer.unsupported("SC1090", expr, "computed namespace reads of unnamed exports"); 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..01801470d 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -190,7 +190,7 @@ export class NarrowingStability { const { call, argument } = slot; const checker = this.lowerer.checker; const signature = checker.getResolvedSignature(call); - const declaration = signature ? checker.signatureDeclaration(signature) : undefined; + const declaration = signature ? signature.declaration : undefined; if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true; const callee = skipOuter(call.expression); if ( @@ -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; @@ -372,7 +372,7 @@ export class NarrowingStability { this.staleBindings.set(symbol, false); let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if (decl !== undefined && ts.isVariableDeclaration(decl)) { if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true; else { @@ -421,7 +421,7 @@ export class NarrowingStability { if (cached !== undefined) return cached; let result = false; try { - const decl = this.lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; const mutableBinding = decl !== undefined && ((ts.isVariableDeclaration(decl) && @@ -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..c9ff3300d 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -163,7 +163,7 @@ function ffiDeclarationDiagnostic( loc: SrcLoc, ): ScrDiagnostic | null { const { binding: bindingDiag, signature: signatureDiag } = ffiFlavor(lowerer); - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const functionDecls = declarations.filter(ts.isFunctionDeclaration); if ( functionDecls.length === 0 || @@ -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", @@ -386,7 +386,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr // are untouched. if (lowerer.libraryCallbacks) { const symbol = lowerer.resolveValueSymbol(expr.expression); - const decls = symbol === null ? [] : lowerer.checker.declarationsOf(symbol); + const decls = symbol === null ? [] : symbol.declarations; const callbackShaped = decls.length > 0 && decls.every( @@ -427,7 +427,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (validSymbols === undefined) throw new PoisonError(); if (!validSymbols.has(symbol)) { if (lowerer.libraryCallbacks) { - const declarations = lowerer.checker.declarationsOf(symbol); + const declarations = symbol.declarations; const programDeclarations = declarations.filter((decl) => libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), ); @@ -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..ddd8d6d56 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -21,15 +21,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { lowerer.collecting = wasCollecting; } if (!symbol) return false; - const owner = lowerer.checker - .declarationsOf(symbol) - .find( - (node) => - (ts.isFunctionDeclaration(node) && node.body !== undefined) || - (ts.isVariableDeclaration(node) && - node.initializer && - ts.isFunctionExpression(node.initializer)), - ); + const owner = symbol.declarations.find( + (node) => + (ts.isFunctionDeclaration(node) && node.body !== undefined) || + (ts.isVariableDeclaration(node) && + node.initializer && + ts.isFunctionExpression(node.initializer)), + ); const declaration = owner && ts.isVariableDeclaration(owner) && @@ -45,7 +43,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 +52,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 +74,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 +102,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..9624ac0aa 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -21,7 +21,7 @@ export function isPrivateSplitConsumer( const local = lowerer.bindingIn(lowerer.ctx, symbol); if (!local || local.boxed || local.tdz || local.mutable || local.id !== iterable.localId) return false; - const decl = lowerer.checker.valueDeclarationOf(symbol); + const decl = symbol.valueDeclaration; if ( !decl || !ts.isVariableDeclaration(decl) || @@ -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 e3b508d86..d3b945f0e 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -22,7 +22,7 @@ import { VOID, arrayOf, } from "../../ir/ir.js"; -import { isNodeTypesPath, requireSpecOf } from "../program.js"; +import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js"; import { ZLIB_CONSTANTS } from "./zlib-constants.js"; /** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */ @@ -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; } @@ -773,6 +773,7 @@ export const STATIC_NUMBER_METHODS: Record< { fns: readonly IrLibFn[]; minArgs: number; maxArgs: number } | undefined > = { toFixed: { fns: ["num.toFixed0", "num.toFixed"], minArgs: 0, maxArgs: 1 }, + toPrecision: { fns: ["num.toPrecision"], minArgs: 0, maxArgs: 1 }, }; /** One lowerable builtin-module FUNCTION: `fn`'s libCall with `params` @@ -1521,6 +1522,7 @@ export const BUILTIN_MODULE_FNS: Record< hash: { fn: "crypto.hashDigestStr", params: [STRING, STRING, STRING], result: STRING }, timingSafeEqual: { fn: "crypto.timingSafeEqual", params: [BYTES_U8, BYTES_U8], result: BOOL }, randomFillSync: { fn: "crypto.randomFill", params: [BYTES_U8, F64, F64], result: BYTES_U8 }, + getRandomValues: { fn: "crypto.getRandomValues", params: [BYTES_U8], result: BYTES_U8 }, randomInt: { fn: "crypto.randomInt", params: [F64, F64], result: F64 }, randomFill: { fn: "crypto.randomFillCb", params: [], result: VOID }, randomUUIDv7: { fn: "crypto.randomUUIDv7", params: [DYN], result: STRING }, @@ -2086,6 +2088,7 @@ export const BUILTIN_MODULE_FN_ALIASES: Record< "crypto.randomFillPromiseDyn", ], timingSafeEqual: ["crypto.timingSafeEqualDyn"], + getRandomValues: ["crypto.getRandomValuesDyn"], randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"], randomUUID: ["crypto.randomUUIDOpts"], }, @@ -2159,6 +2162,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["blob.fileNew"], note: "a Blob with name and lastModified (Date.now() when absent)", }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], + note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + }, + { + id: "stdlib.webcrypto.CryptoKey", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, { id: "stdlib.atomics", kind: "stdlib", @@ -2328,6 +2345,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", @@ -2644,6 +2680,15 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["os.constants"], note: "the platform's dlopen, errno, signals and priority groups; members a platform lacks read as undefined", }, + // fs.constants as a value, and its members beyond the baked access-mode + // bits: the target libc's null-prototype constants object. + { + id: "node-builtin.fs.constants", + kind: "node-builtin", + name: "fs.constants", + fns: ["fs.constants"], + note: "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined", + }, ]; /** The win32-target overrides of the bare modules' constants: path's @@ -3201,7 +3246,7 @@ export function isChildSurfaceMember( ): boolean { if (isStdlibMember(lowerer, access)) return true; const sym = lowerer.checker.getSymbolAtLocation(access.name); - const decl = sym ? lowerer.checker.declarationsOf(sym)[0] : undefined; + const decl = sym ? sym.declarations[0] : undefined; const iface = decl?.parent; if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false; if (lowerer.isStdlibFile(decl.getSourceFile())) return false; @@ -3214,10 +3259,7 @@ export function isChildSurfaceMember( * with lib interfaces, so a member can carry declarations from both. A * user's own declaration is in neither, so shadowing never matches. */ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean { - return ( - !!symbol && - lowerer.checker.declarationsOf(symbol).some((d) => lowerer.isStdlibFile(d.getSourceFile())) - ); + return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())); } /** The canonical stdlib-global name `expr` denotes, or null. Three @@ -3243,9 +3285,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin // symbol has no ordinary declarations for the provenance check. if ( expr.text === "globalThis" && - (!symbol || - lowerer.checker.declarationsOf(symbol).length === 0 || - lowerer.isStdlibSymbol(symbol)) + (!symbol || symbol.declarations.length === 0 || lowerer.isStdlibSymbol(symbol)) ) return "globalThis"; if (!symbol) return null; @@ -3253,7 +3293,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if (alias !== undefined) return alias; const classAlias = lowerer.builtinClassAliasOf(symbol); if (classAlias?.kind === "collection") return classAlias.which; - const decl = lowerer.checker.declarationsOf(symbol)[0]; + const decl = symbol.declarations[0]; if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) { const importDecl = decl.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -3269,7 +3309,21 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin // `Buffer` resolves to a user-file declaration and still misses. const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; - if (!lowerer.isStdlibSymbol(resolved)) return null; + if (!lowerer.isStdlibSymbol(resolved)) { + // A CommonJS binding of the buffer module's Buffer (`var Buffer = + // require('buffer').Buffer`, `const { Buffer } = require('buffer')` + // — safe-buffer and compression): the module member IS the global + // constructor. + if ( + decl !== undefined && + (ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) && + isJsSourceFile(decl.getSourceFile()) + ) { + const imported = lowerer.builtinImportOf(expr); + if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer"; + } + return null; + } return resolved.name === "global" ? "globalThis" : resolved.name; } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { @@ -3344,7 +3398,9 @@ export function stdlibGlobalAliasNameOf( const name = requireSpec === "process" || requireSpec === "node:process" ? "process" - : stdlibGlobalNameOf(lowerer, aliasExpr); + : bufferNamespaceMember(lowerer, aliasExpr) + ? "Buffer" + : stdlibGlobalNameOf(lowerer, aliasExpr); if (name === null) return null; // These globals have provenance-aware call or member lowering, so a // stable snapshot keeps the same dispatch as the original global. @@ -3365,6 +3421,14 @@ export function stdlibGlobalAliasNameOf( : null; } +/** `buffer.Buffer` through a buffer-module namespace binding (`var buffer + * = require('buffer')`): the module's Buffer IS the global constructor. */ +export function bufferNamespaceMember(lowerer: Lowerer, expr: ts.Expression): boolean { + if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "Buffer") return false; + const member = lowerer.builtinMemberOf(expr); + return member?.module === "buffer" && member.member === "Buffer"; +} + export function stdlibGlobalAliasDecl( lowerer: Lowerer, nameNode: ts.Node, @@ -3392,7 +3456,7 @@ export function stdlibGlobalAliasDecl( * the SC2020 fence's wording: "typed by @types/node", not "standard * library". */ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | undefined): boolean { - const decls = sym ? lowerer.checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return false; let viaNode = false; for (const d of decls) { diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index d3bd70d2c..5d7a20c06 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -26,7 +26,7 @@ function constantSymbolDescription( const symbol = lowerer.resolveValueSymbol(name); if (!symbol || seen.has(symbol)) return null; seen.add(symbol); - const declaration = lowerer.checker.valueDeclarationOf(symbol); + const declaration = symbol.valueDeclaration; if ( !declaration || !ts.isVariableDeclaration(declaration) || @@ -94,8 +94,8 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const cached = cache.get(sym); if (cached !== undefined) return cached; cache.set(sym, null); - const decls = lowerer.checker.declarationsOf(sym); - const decl = lowerer.checker.valueDeclarationOf(sym); + const decls = sym.declarations; + const decl = sym.valueDeclaration; if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null; const list = decl.parent; @@ -109,12 +109,12 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) { const target = classSymbolKeyOf(lowerer, init); if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); if ( bindingEarlyUse7( lowerer.program, decl.getSourceFile(), - decl.getSourceFile().statements.indexOf(list.parent), + decl.getSourceFile().statements.nodes.indexOf(list.parent), decl, preceding, ) !== null @@ -131,10 +131,10 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const root = registered ? callee.expression : callee; if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null; const arg = - init.arguments.length === 0 + init.arguments.nodes.length === 0 ? null - : init.arguments.length === 1 - ? init.arguments[0]! + : init.arguments.nodes.length === 1 + ? init.arguments.nodes[0]! : undefined; if (arg === undefined || (registered && arg === null)) return null; const description = arg === null ? "" : constantSymbolDescription(lowerer, arg); @@ -143,7 +143,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS // module-scope declaration above can name one fixed layout slot. const sf = decl.getSourceFile(); if (bindingWritten(lowerer, sym, sf)) return null; - const preceding = list.declarations.slice(0, list.declarations.indexOf(decl)); + const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); // Creating a function value does not execute its body. The general // require-publication analysis conservatively enters those bodies, but a // prefix containing only imports and function definitions cannot call @@ -164,19 +164,24 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS ); }; const prefixIsInert = - sf.statements - .slice(0, sf.statements.indexOf(list.parent)) + sf.statements.nodes + .slice(0, sf.statements.nodes.indexOf(list.parent)) .every( (statement) => ts.isImportDeclaration(statement) || ts.isFunctionDeclaration(statement) || (ts.isVariableStatement(statement) && - statement.declarationList.declarations.every(inertInitializer)), + statement.declarationList.declarations.nodes.every(inertInitializer)), ) && preceding.every(inertInitializer); if ( !prefixIsInert && - bindingEarlyUse7(lowerer.program, sf, sf.statements.indexOf(list.parent), decl, preceding) !== - null + bindingEarlyUse7( + lowerer.program, + sf, + sf.statements.nodes.indexOf(list.parent), + decl, + preceding, + ) !== null ) return null; // A registry name denotes one symbol even when several source bindings diff --git a/packages/compiler/src/frontend/lowering/symbol-methods.ts b/packages/compiler/src/frontend/lowering/symbol-methods.ts index 093ad058b..f6a8e68e4 100644 --- a/packages/compiler/src/frontend/lowering/symbol-methods.ts +++ b/packages/compiler/src/frontend/lowering/symbol-methods.ts @@ -26,6 +26,6 @@ export function lowerClassSymbolMethodCall( if (!method) return null; const loc = locOf(call); const receiver = lowerer.lowerExpr(access.expression); - const args = lowerer.completeArgs(call.arguments, method.sig.params, loc, call); + const args = lowerer.completeArgs(call.arguments.nodes, method.sig.params, loc, call); return accessorCall(lowerer, type.className, name, receiver, args, method.sig.ret, loc); } diff --git a/packages/compiler/src/frontend/lowering/thread-slots.ts b/packages/compiler/src/frontend/lowering/thread-slots.ts index 9247703f5..50b5ecf09 100644 --- a/packages/compiler/src/frontend/lowering/thread-slots.ts +++ b/packages/compiler/src/frontend/lowering/thread-slots.ts @@ -83,7 +83,7 @@ function rootOf(info: ClassInfo): ClassInfo { function familyOf(lowerer: Lowerer, expr: ts.NewExpression): Family { const known = lowerer.threadSlotFamilies.get(expr); if (known !== undefined) return known; - const args = expr.arguments ?? []; + const args = expr.arguments?.nodes ?? []; const arg = args[0]; if (args.length !== 1 || arg === undefined || ts.isSpreadElement(arg)) lowerer.noLowering(`new ThreadSlots with ${args.length} arguments`, expr); @@ -168,7 +168,7 @@ function declaringNew(lowerer: Lowerer, receiver: ts.Expression): ts.NewExpressi 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; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isVariableDeclaration(decl)) return null; const list = decl.parent; if ( @@ -211,8 +211,11 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): if (callee.questionDotToken !== undefined || expr.questionDotToken !== undefined) lowerer.noLowering(`ThreadSlots.${member} through ?.`, expr, `call slots.${member}() directly`); const arity = member === "set" ? 2 : 1; - if (expr.arguments.length !== arity || expr.arguments.some((a) => ts.isSpreadElement(a))) - lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.length} arguments`, expr); + if ( + expr.arguments.nodes.length !== arity || + expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) + ) + lowerer.noLowering(`ThreadSlots.${member} with ${expr.arguments.nodes.length} arguments`, expr); const init = declaringNew(lowerer, callee.expression); if (init === null) lowerer.unsupported( @@ -229,7 +232,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): lowerer.lowerExpr(callee.expression), slotsType, ); - const keyNode = expr.arguments[0]!; + const keyNode = expr.arguments.nodes[0]!; const key = lowerer.coerceInto(keyNode, lowerer.lowerExpr(keyNode), family.key); const name: IrExpr = { kind: "strLit", value: family.name, type: STRING, loc }; const args = [name, receiver, key]; @@ -244,7 +247,7 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression): case "delete": return { kind: "intrinsic", name: `slots.${member}`, args, type: { kind: "bool" }, loc }; case "set": { - const valueNode = expr.arguments[1]!; + const valueNode = expr.arguments.nodes[1]!; const value = lowerer.coerceInto(valueNode, lowerer.lowerExpr(valueNode), family.value); // `set` returns the slots; a statement discards them. const type = ts.isExpressionStatement(expr.parent) ? VOID : slotsType; diff --git a/packages/compiler/src/frontend/lowering/union-retag.test.ts b/packages/compiler/src/frontend/lowering/union-retag.test.ts index 0cb7b1b9f..8116d6378 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.test.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "vitest"; import { BOOL, F64, NULL_T, STRING, UNDEFINED_T, typeEquals, typeKey } from "../../ir/ir.js"; import type { IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../../ir/ir.js"; -import { planUnionRetag, buildUnionRetag, planRecordUnionWrap } from "./union-retag.js"; +import { + planUnionRetag, + buildUnionRetag, + planRecordUnionLift, + planRecordUnionWrap, +} from "./union-retag.js"; import type { UnionRetagArm } from "./union-retag.js"; import type { WidthLift } from "./width-lift.js"; @@ -126,6 +131,30 @@ describe("record conversion with shared layouts", () => { f.shapes[2]!.fields.reverse(); expect(planRecordUnionWrap(f.shapes[1]!, f.to, f.shapeOf)).toBeNull(); }); + + test("routes a record that fits several arms by its discriminant", () => { + const f = fixture(); + expect(planRecordUnionLift(f.shapes[0]!, f.to, f.shapeOf, f.lift)).toEqual({ + field: "kind", + fieldType: STRING, + routes: [ + { tag: 0, lift: { how: "width" }, values: ["left"] }, + { tag: 1, lift: { how: "width" }, values: ["right"] }, + ], + }); + // Without the discriminant field, or without an arm it converts into, + // there is no route. + const unnamed: IrRecordShape = { id: "r3", fields: f.shapes[0]!.fields.slice(1) }; + expect(planRecordUnionLift(unnamed, f.to, f.shapeOf, f.lift)).toBeNull(); + const unrelated: IrRecordShape = { + id: "r4", + fields: [ + { name: "kind", type: STRING }, + { name: "other", type: F64 }, + ], + }; + expect(planRecordUnionLift(unrelated, f.to, f.shapeOf, f.lift)).toBeNull(); + }); }); describe("union conversion planning", () => { diff --git a/packages/compiler/src/frontend/lowering/union-retag.ts b/packages/compiler/src/frontend/lowering/union-retag.ts index 8d1c61d09..1c435ab36 100644 --- a/packages/compiler/src/frontend/lowering/union-retag.ts +++ b/packages/compiler/src/frontend/lowering/union-retag.ts @@ -86,6 +86,36 @@ export function planRecordUnionWrap( return { field: discriminant.field, fieldType, routes }; } +/** A record whose layout converts into more than one arm of a + * discriminated union. The IR keeps the discriminant field as a plain + * string, so its layout alone cannot say which variant it is; the value of + * the discriminant does, as in JavaScript. Every record arm the record + * converts into (by copy or width) becomes a route selected by that arm's + * discriminant values. A value no route names throws the catchable + * TypeError. */ +export function planRecordUnionLift( + source: IrRecordShape, + to: IrUnionDef, + shapeOf: (id: string) => IrRecordShape | undefined, + widthLift: (src: IrType, dst: IrType) => WidthLift | null, +): RecordUnionWrapPlan | null { + const discriminant = to.discriminant; + if (!discriminant || source.tuple || source.indexValue || shapeHasAccessorSlots(source)) + return null; + const fieldType = discriminantField(source, discriminant.field); + if (!fieldType || discriminantOwners(to, shapeOf) === null) return null; + const sourceType: IrType = { kind: "record", shapeId: source.id }; + const routes: UnionRetagRoute[] = []; + for (const entry of discriminant.cases) { + const arm = to.arms[entry.tag]; + if (!arm || arm.kind !== "record") continue; + const lift = arm.shapeId === source.id ? { how: "copy" as const } : widthLift(sourceType, arm); + if (lift === null) continue; + routes.push({ tag: entry.tag, lift, values: entry.values }); + } + return routes.length > 0 ? { field: discriminant.field, fieldType, routes } : null; +} + /** Pure conversion planning. A source storage tag need not denote just * one semantic variant: refinements can coalesce recursive record layouts. * When both unions name the same discriminant, its values select target 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/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts index 40c533d72..d90f9ed35 100644 --- a/packages/compiler/src/frontend/lowering/width-lift.ts +++ b/packages/compiler/src/frontend/lowering/width-lift.ts @@ -7,6 +7,7 @@ export type WidthLift = | { how: "copy" } | { how: "wrap"; tag: number } | { how: "discriminantWrap" } + | { how: "discriminantRoute" } | { how: "retag" } | { how: "liftWrap"; tag: number; arm: IrType } | { how: "width" } 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..04c66a559 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -16,14 +16,12 @@ export function isNpmStaticSubclassArgument( const symbol = type.getSymbol(); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some( - (declaration) => - (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && - npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, - ) + symbol.declarations.some( + (declaration) => + (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && + /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && + npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, + ) ); }; const subtype = (actual: ts.Type, expected: ts.Type): boolean => { @@ -55,15 +53,15 @@ export function isNpmStaticSubclassArgument( if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip"; if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined; const index = - node.arguments?.findIndex( + node.arguments?.nodes.findIndex( (argument) => argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end, ) ?? -1; if (index < 0) return undefined; const parameter = checker.getResolvedSignature(node)?.getParameters()[index]; - if (parameter && node.arguments) + if (parameter && node.arguments?.nodes) accepted = subtype( - checker.getTypeAtLocation(node.arguments[index]!), + checker.getTypeAtLocation(node.arguments.nodes[index]!), checker.getTypeOfSymbol(parameter), ); return undefined; diff --git a/packages/compiler/src/frontend/pipeline.test.ts b/packages/compiler/src/frontend/pipeline.test.ts index 902441c96..6140d2948 100644 --- a/packages/compiler/src/frontend/pipeline.test.ts +++ b/packages/compiler/src/frontend/pipeline.test.ts @@ -14,7 +14,7 @@ const loader: ProgramLoader = () => { // Resource-ownership tests inject only the load fields the orchestration // reads. Real program loading and native execution have integration tests. const load = { - entry: { fileName: "/main.ts", text: "", statements: [] }, + entry: { fileName: "/main.ts", text: "", statements: { pos: 0, end: 0, nodes: [] } }, moduleOrder: [], dispose: vi.fn(), } as unknown as ReturnType; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index f7ab33429..024c69d98 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -146,7 +146,7 @@ function detectAutoPackages( const owner = npmStaticPackageOfPath(sf.fileName); const viaTransitive = owner !== null && transitive.has(owner); const edges: { spec: string; loc: SrcLoc }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if ( (isImportDeclaration(stmt) || isExportDeclaration(stmt)) && stmt.moduleSpecifier && @@ -167,7 +167,7 @@ function detectAutoPackages( // anywhere in the package, nested ones included. if (viaTransitive) { for (const call of moduleSourceCandidates(load.program, sf).calls) { - const arg = call.arguments[0]; + const arg = call.arguments.nodes[0]; if ( arg !== undefined && isStringLiteralLike(arg) && diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 8fec2f486..c1bd183f2 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -344,8 +344,8 @@ test("repeated cycle edges reuse a file's binding index", () => { try { const main = load.entry; const peer = load.program.getSourceFile(join(directory, "peer.ts"))!; - const mainImports = main.statements.filter(ts.isImportDeclaration); - const peerImports = peer.statements.filter(ts.isImportDeclaration); + const mainImports = main.statements.nodes.filter(ts.isImportDeclaration); + const peerImports = peer.statements.nodes.filter(ts.isImportDeclaration); const edges = new Map([ [main, mainImports.map((stmt) => ({ dep: peer, stmt }))], [peer, peerImports.map((stmt) => ({ dep: main, stmt }))], diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 7d443318d..c7bee9c6c 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)) { @@ -1225,9 +1225,9 @@ function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const symbol = checker.getSymbolAtLocation(value); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent) + symbol.declarations.some( + (decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent, + ) ); } @@ -1236,9 +1236,9 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { const symbol = checker.getSymbolAtLocation(ident); return ( symbol !== undefined && - checker - .declarationsOf(symbol) - .some((decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl)) + symbol.declarations.some( + (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl), + ) ); } @@ -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) && @@ -1953,7 +1953,7 @@ function stripRequireCasts7(expr: ts.Expression): ts.Expression { function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return false; const importDecl = decl.parent?.parent?.parent; if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) { @@ -1963,7 +1963,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool } if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { const target = checker.getAliasedSymbol(symbol); - const targetDecl = checker.declarationsOf(target)[0]; + const targetDecl = target.declarations[0]; if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) { for ( let p: ts.Node | undefined = targetDecl.parent; @@ -1988,9 +1988,7 @@ export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifi const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(callee); const decl = symbol - ? checker - .declarationsOf(symbol) - .find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) + ? symbol.declarations.find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d)) : undefined; if ( decl === undefined || @@ -2032,8 +2030,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]!) ); } @@ -2054,7 +2052,7 @@ function stableCreateRequireBindingReason7( } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(decl.name); - if (!symbol || checker.declarationsOf(symbol).length !== 1) + if (!symbol || symbol.declarations.length !== 1) return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; @@ -2072,11 +2070,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 +2093,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 +2124,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"; @@ -2202,7 +2202,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } @@ -2217,7 +2217,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 +2239,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 +2255,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; } @@ -2357,7 +2357,7 @@ function selfImportTdzFences7( // The aliased declaration's TDZ horizon: the position after which reads // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { - const d = checker.valueDeclarationOf(aliased) ?? checker.declarationsOf(aliased)[0]; + const d = aliased.valueDeclaration ?? aliased.declarations[0]; if (d === undefined || d.getSourceFile() !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; return d.getEnd(); @@ -2394,7 +2394,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 +2572,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; } @@ -2614,11 +2614,11 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod let sym = checker.getSymbolAtLocation(root); if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); - const decls = checker.declarationsOf(sym); + const decls = sym.declarations; 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 +2636,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 +2699,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 +2723,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 +2733,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 +2765,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 +2824,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 +3020,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 +3032,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 +3040,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 +3084,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 +3105,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 +3240,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 +3434,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 +3446,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 +3618,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 +3634,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 +3649,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 +3659,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 +3706,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 +3717,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 +3797,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 +3807,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); @@ -3834,7 +3838,7 @@ function returnsOnlyClosuresOver7( if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } - const decls = symbol === undefined ? [] : checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; if (decl.getSourceFile().isDeclarationFile) return false; @@ -3848,10 +3852,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 +3902,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); @@ -3956,7 +3961,7 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea symbol = checker.getAliasedSymbol(symbol); } if (symbol === undefined) return false; - const decls = checker.declarationsOf(symbol); + const decls = symbol.declarations; if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; @@ -4027,7 +4032,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 +4045,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 +4279,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 +4490,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 +4541,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 +4903,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 +4992,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 +5368,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 +5381,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 +5392,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 +5416,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 +5461,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 ( @@ -5575,18 +5580,16 @@ function analyzeEsmNamedImportLinks( if ( name === "default" && hasNodeTsDefaultInterfacePlaceholder7(dep) && - checker - .declarationsOf(resolved) - .some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && - ts - .getModifiers(declaration) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, - ) + resolved.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true && + ts + .getModifiers(declaration) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true, + ) ) { return resolved; } @@ -5600,7 +5603,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 +5626,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 +5643,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 +5670,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 +5679,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 +5697,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 +5795,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 +5816,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 +5825,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 +5893,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 +5906,7 @@ export function orderedImportsOf( sf: ts.SourceFile, ): { stmt: ts.Statement; dep: ts.SourceFile | null }[] { const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = []; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue; if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue; if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue; @@ -6025,7 +6028,7 @@ export function cjsVoidInitPreambleReals( Extract, { kind: "member" }> >(); for (const name of names) { - const real = sf.statements + const real = sf.statements.nodes .map((s) => cjsExportAssignmentOf(s)) .find( (other) => @@ -6085,7 +6088,7 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null { if (!ts.isSourceFile(sf)) return null; const mine = cjsExportAssignmentOf(stmt); if (!mine) return null; - const stmts = sf.statements; + const stmts = sf.statements.nodes; let lastTable = -1; for (let i = 0; i < stmts.length; i++) { if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i; @@ -6123,15 +6126,15 @@ export function cjsExportTargetLiteral( ts.isNewExpression(e) && ts.isIdentifier(e.expression) && e.expression.text === "Proxy" && - (e.arguments?.length ?? 0) >= 1 + (e.arguments?.nodes.length ?? 0) >= 1 ) { - return cjsExportTargetLiteral(e.arguments![0]!, sf); + return cjsExportTargetLiteral(e.arguments!.nodes[0]!, sf); } if (ts.isIdentifier(e)) { - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { if (!ts.isVariableStatement(stmt)) continue; if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) continue; - for (const d of stmt.declarationList.declarations) { + for (const d of stmt.declarationList.declarations.nodes) { if ( d.name !== undefined && ts.isIdentifier(d.name) && @@ -6157,7 +6160,7 @@ export function cjsClassExprWholeExportOf( sf: ts.SourceFile, ): { classExpr: ts.ClassExpression; stmt: ts.Statement } | null { let last: { classExpr: ts.ClassExpression; stmt: ts.Statement } | null = null; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (!cjs || cjs.kind !== "table") continue; let rhs: ts.Expression = cjs.expr.right; @@ -6190,7 +6193,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; const sf = obj.getSourceFile(); if (!isJsSourceFile(sf)) return false; - for (const stmt of sf.statements) { + for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) { return cjsExportTargetLiteral(cjs.expr.right, sf) === obj; @@ -6294,7 +6297,7 @@ function receiverRoot(text: string, pos: number): string | null { /** Whether `name` is bound in `fileName` by an import of a Node builtin. */ function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean { const sf = load.program.getSourceFile(fileName); - for (const stmt of sf?.statements ?? []) { + for (const stmt of sf?.statements?.nodes ?? []) { if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue; const clause = stmt.importClause; @@ -6304,7 +6307,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/threads-import.ts b/packages/compiler/src/frontend/threads-import.ts index 12425c360..1944b2b81 100644 --- a/packages/compiler/src/frontend/threads-import.ts +++ b/packages/compiler/src/frontend/threads-import.ts @@ -11,7 +11,7 @@ export type ThreadsMember = "publish" | "sharesPublishedGraphs"; export function threadsImportOf(program: ts.Program, ident: ts.Identifier): ThreadsMember | null { const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(ident); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !ts.isImportSpecifier(decl)) return null; const importDecl = decl.parent?.parent?.parent; if ( diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index ded7c5225..2ed5c8c9b 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; } @@ -474,7 +474,7 @@ class UnitResolver { let symbol = this.checker.getSymbolAtLocation(root); if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = this.checker.getAliasedSymbol(symbol); - const decls = symbol === undefined ? [] : this.checker.declarationsOf(symbol); + const decls = symbol === undefined ? [] : symbol.declarations; const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile); if (decl === undefined) { // CommonJS `exports`/`module` as values: the module object. 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-cache.ts b/packages/compiler/src/frontend/ts7/checker-cache.ts index b9a455a0d..1613272e1 100644 --- a/packages/compiler/src/frontend/ts7/checker-cache.ts +++ b/packages/compiler/src/frontend/ts7/checker-cache.ts @@ -44,9 +44,6 @@ export class CheckerCache { readonly managedSymbols = new Set(); readonly intrinsics = new Map(); readonly tupleTypeAnswer = new Map(); - readonly declsOf = new Map(); - readonly valueDeclOf = new Map(); - readonly sigDeclOf = new Map(); readonly callSigsOf = new Map(); readonly ctorSigsOf = new Map(); readonly constantValueOf = new Map(); @@ -90,9 +87,6 @@ export class CheckerCache { this.managedSymbols.clear(); this.intrinsics.clear(); this.tupleTypeAnswer.clear(); - this.declsOf.clear(); - this.valueDeclOf.clear(); - this.sigDeclOf.clear(); this.callSigsOf.clear(); this.ctorSigsOf.clear(); this.constantValueOf.clear(); diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index 53b960b91..7b3647b2d 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}`); @@ -80,7 +80,7 @@ test("unchanged AST identities receive independent semantic answers after a depe const oldType = old.getTypeAtLocation(name); const oldSymbol = old.getSymbolAtLocation(name)!; expect(old.typeToString(oldType)).toBe("number"); - expect(old.valueDeclarationOf(oldSymbol)).toBe(declaration(root, "result")); + expect(oldSymbol.valueDeclaration).toBe(declaration(root, "result")); h.paths.set(h.path("dependency.ts"), 'export const value: string = "changed";'); const second = h.session.updateSnapshot({ @@ -165,9 +165,6 @@ test("all public query and prefetch entry points reject a disposed facade before facade.typeToString(type); facade.prefetchSourceFile(root); const calls: Record unknown> = { - declarationsOf: () => facade.declarationsOf(symbol), - valueDeclarationOf: () => facade.valueDeclarationOf(symbol), - signatureDeclaration: () => facade.signatureDeclaration(signature), getCallSignatures: () => facade.getCallSignatures(type), getConstructSignatures: () => facade.getConstructSignatures(type), getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index bf83b367c..41eb0d0b9 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -1,5 +1,4 @@ import { CheckerCache } from "./checker-cache.js"; -import { InternalCompilerError } from "../../errors.js"; /* The checker facade: 5.9.3-shaped TypeChecker methods over 7.0.2's sync * client, built around the survey's feasibility verdict. Naive per-call use * of the 7.0.2 client costs 0.1-0.3 ms of IPC per query; the census counted @@ -173,7 +172,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) ); } @@ -238,60 +237,6 @@ export class CheckerFacade { this.cache.dispose(); } - /* ── the symbol-declaration surface (phase 3) ───────────────────────── - * 7's Symbol carries declarations as NodeHandles (server references), - * where 5.9.3 handed out the nodes themselves. The lowering reads - * symbol.declarations/valueDeclaration pervasively, so the facade owns - * the resolve step (NodeHandle.resolve into the client AST — identity- - * stable, probe-verified) and memoizes per symbol. Requires the project - * the symbols came from (options.project — Ts7Program supplies it). */ - - private requireProject(): Project { - const project = this.options.project; - if (!project) - throw new InternalCompilerError( - "CheckerFacade built without a project cannot resolve declarations", - ); - return project; - } - - /** 5.9.3's symbol.declarations (never undefined here: 7 answers an empty - * array where 5.9.3 answered undefined — callers treat them alike). */ - declarationsOf(symbol: Ts7Symbol): readonly Node[] { - this.ensureActive(); - let decls = this.cache.declsOf.get(symbol); - if (decls === undefined) { - const project = this.requireProject(); - const resolved: Node[] = []; - for (const handle of symbol.declarations) { - const node = handle.resolve(project); - if (node !== undefined) resolved.push(node); - } - decls = resolved; - this.cache.declsOf.set(symbol, decls); - } - return decls; - } - - /** 5.9.3's symbol.valueDeclaration. */ - valueDeclarationOf(symbol: Ts7Symbol): Node | undefined { - this.ensureActive(); - if (this.cache.valueDeclOf.has(symbol)) return this.cache.valueDeclOf.get(symbol); - const decl = symbol.valueDeclaration?.resolve(this.requireProject()); - this.cache.valueDeclOf.set(symbol, decl); - return decl; - } - - /** 5.9.3's signature.getDeclaration() (undefined for synthesized - * signatures — same contract as sig.declaration there). */ - signatureDeclaration(signature: Signature): Node | undefined { - this.ensureActive(); - if (this.cache.sigDeclOf.has(signature)) return this.cache.sigDeclOf.get(signature); - const decl = signature.declaration?.resolve(this.requireProject()); - this.cache.sigDeclOf.set(signature, decl); - return decl; - } - /** 5.9.3's type.getCallSignatures(). */ getCallSignatures(type: Type): readonly Signature[] { this.ensureActive(); 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 095021922..4920c96ae 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -294,8 +294,12 @@ export class SemanticSymbol { readonly name: string; readonly flags: number; readonly checkFlags: number; - readonly declarations: SemanticNodeHandle[]; - readonly valueDeclaration: SemanticNodeHandle | undefined; + /** The declarations' node handles, as the server sends them. */ + readonly declarationHandles: SemanticNodeHandle[]; + readonly valueDeclarationHandle: SemanticNodeHandle | undefined; + private declarationNodes: readonly AstNode[] | undefined; + private valueDeclarationNode: AstNode | undefined; + private valueDeclarationResolved = false; private readonly parent: number | undefined; private readonly exportSymbol: number | undefined; private membersCache: Map | undefined; @@ -310,16 +314,40 @@ export class SemanticSymbol { this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name; this.flags = data.flags; this.checkFlags = data.checkFlags; - this.declarations = (data.declarations ?? []).map( + this.declarationHandles = (data.declarations ?? []).map( (handle) => new SemanticNodeHandle(handle, canonicalProject), ); - this.valueDeclaration = data.valueDeclaration + this.valueDeclarationHandle = data.valueDeclaration ? new SemanticNodeHandle(data.valueDeclaration, canonicalProject) : undefined; this.parent = data.parent; this.exportSymbol = data.exportSymbol; } + /** The declaration nodes, resolved in the symbol's project. A handle whose + * file the project doesn't have resolves to nothing and is left out. */ + get declarations(): readonly AstNode[] { + this.canonicalProject.ensureActive(); + if (this.declarationNodes === undefined) { + const nodes: AstNode[] = []; + for (const handle of this.declarationHandles) { + const node = handle.resolve(); + if (node !== undefined) nodes.push(node); + } + this.declarationNodes = nodes; + } + return this.declarationNodes; + } + + get valueDeclaration(): AstNode | undefined { + this.canonicalProject.ensureActive(); + if (!this.valueDeclarationResolved) { + this.valueDeclarationNode = this.valueDeclarationHandle?.resolve(); + this.valueDeclarationResolved = true; + } + return this.valueDeclarationNode; + } + getParent(): SemanticSymbol | undefined { return this.canonicalProject.fetchSymbol(this.id, "getParentOfSymbol", this.parent); } @@ -603,7 +631,10 @@ export class SemanticType { export class SemanticSignature { readonly id: number; readonly flags: number; - readonly declaration: SemanticNodeHandle | undefined; + /** The declaration's node handle, as the server sends it. */ + readonly declarationHandle: SemanticNodeHandle | undefined; + private declarationNode: AstNode | undefined; + private declarationResolved = false; constructor( private readonly data: SignatureResponse, @@ -611,11 +642,22 @@ export class SemanticSignature { ) { this.id = data.id; this.flags = data.flags; - this.declaration = data.declaration + this.declarationHandle = data.declaration ? new SemanticNodeHandle(data.declaration, project) : undefined; } + /** The declaration node, resolved in the signature's project; undefined + * for a signature the checker synthesized. */ + get declaration(): AstNode | undefined { + this.project.ensureActive(); + if (!this.declarationResolved) { + this.declarationNode = this.declarationHandle?.resolve(); + this.declarationResolved = true; + } + return this.declarationNode; + } + getTypeParameters(): SemanticType[] { return this.project.fetchTypes( this.id, 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 41d18a6a2..3084e15c6 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -125,6 +125,19 @@ export function jsOpenObjectType( : type; } +/** The WebCrypto interfaces whose values are checked-dynamic: the crypto + * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries + * (scr_webcrypto.c). */ +export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ + "Crypto", + "SubtleCrypto", + "CryptoKey", + "Algorithm", + "KeyAlgorithm", + "HmacImportParams", + "HmacKeyAlgorithm", +]); + export const FETCH_AMBIENT_TYPES = [ "Response", "Request", @@ -930,13 +943,11 @@ export function isWebAssemblyType( ): boolean { const symbol = type.getAliasSymbol() ?? type.getSymbol(); if (!symbol) return false; - return ctx.checker - .declarationsOf(symbol) - .some( - (d) => - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), - ); + return symbol.declarations.some( + (d) => + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), + ); } /** True when a type contains a record anywhere (the trigger for the @@ -1055,6 +1066,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; @@ -1205,7 +1219,7 @@ export function isLegacyUrlSymbol( ctx: Pick, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { if (!ctx.isStdlibFile(d.getSourceFile())) continue; let family = false; for (let n: ts.Node | undefined = d; n; n = n.parent) { @@ -1242,7 +1256,7 @@ export function isOsConstantsSymbol( types = false, ): boolean { if (!symbol) return false; - for (const d of ctx.checker.declarationsOf(symbol)) { + for (const d of symbol.declarations) { const alias = types && ts.isTypeAliasDeclaration(d) && d.name.text === "SignalConstants"; const signals = !types && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "signals"; @@ -1273,14 +1287,12 @@ function isWorkerHandleSymbol( symbol?.name === "MessagePort" || symbol?.name === "MessageChannel" || symbol?.name === "BroadcastChannel") && - ctx.checker - .declarationsOf(symbol) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "worker_threads"), - ) + symbol.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "worker_threads"), + ) ); } @@ -1294,9 +1306,7 @@ function isReceiveMessageResult( let found = false; for (const member of members) { if (member.flags & ts.TypeFlags.Undefined) continue; - const decl = member.getSymbol() - ? ctx.checker.declarationsOf(member.getSymbol()!)[0] - : undefined; + const decl = member.getSymbol() ? member.getSymbol()!.declarations[0] : undefined; let fn: ts.Node | undefined = decl?.parent; while (fn && ts.isUnionTypeNode(fn)) fn = fn.parent; if ( @@ -1355,13 +1365,11 @@ export function isFetchHandleType( 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()), - ) + !symbol.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return false; } @@ -1436,15 +1444,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); - if (intrinsic?.name === "globalThis" && checker.declarationsOf(intrinsic).length === 0) - return DYN; + if (intrinsic?.name === "globalThis" && intrinsic.declarations.length === 0) return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0; const classInstance = (intrinsic.flags & ts.SymbolFlags.Class) !== 0 && type.isClassOrInterface(); if ( (objectLiteral || classInstance) && - checker.declarationsOf(intrinsic).some((decl) => ctx.jsDynamicDeclaration!(decl)) + intrinsic.declarations.some((decl) => ctx.jsDynamicDeclaration!(decl)) ) return DYN; } @@ -1462,7 +1469,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const directAlias = type.getAliasSymbol(); if ( directAlias?.name === "ExecFileException" && - checker.declarationsOf(directAlias).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + directAlias.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ) { return { kind: "object", className: "%Error" }; } @@ -1472,14 +1479,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const execException = type.getSymbol(); if ( execException?.name === "ExecException" && - checker - .declarationsOf(execException) - .some( - (decl) => - ts.isInterfaceDeclaration(decl) && - ctx.isStdlibFile(decl.getSourceFile()) && - isDeclaredInAmbientModule(decl, "child_process"), - ) + execException.declarations.some( + (decl) => + ts.isInterfaceDeclaration(decl) && + ctx.isStdlibFile(decl.getSourceFile()) && + isDeclaredInAmbientModule(decl, "child_process"), + ) ) { return { kind: "object", className: "%Error" }; } @@ -1575,7 +1580,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const errorOptionsSymbol = widened.getSymbol(); if ( errorOptionsSymbol?.name === "ErrorOptions" && - checker.declarationsOf(errorOptionsSymbol).some((d) => ctx.isStdlibFile(d.getSourceFile())) + errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -1607,17 +1612,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( parseArgsSym && PARSE_ARGS_DYN_TYPES.has(parseArgsSym.name) && - checker.declarationsOf(parseArgsSym).some(belongs) + parseArgsSym.declarations.some(belongs) ) return DYN; // ReturnType and instantiated conditional types can erase the alias. // Require every member to retain the util declaration provenance so // unrelated records with similar property names keep their own layout. const members = checker.getPropertiesOfType(widened); - if ( - members.length > 0 && - members.every((member) => checker.declarationsOf(member).some(belongs)) - ) + if (members.length > 0 && members.every((member) => member.declarations.some(belongs))) return DYN; } // The lib's BOXED wrapper interfaces used as TYPES (`const n: Number = @@ -1630,7 +1632,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( boxSym && (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") && - checker.declarationsOf(boxSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + boxSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL; } @@ -1653,7 +1655,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctorSym.name === "StringConstructor" || ctorSym.name === "NumberConstructor" || ctorSym.name === "BooleanConstructor") && - checker.declarationsOf(ctorSym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + ctorSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return funcOf( [STRING], @@ -1765,7 +1767,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const classSym = widened.getSymbol(); const name = classSym?.name; if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { - for (const d of checker.declarationsOf(classSym!)) { + for (const d of classSym!.declarations) { if (!ts.isClassDeclaration(d)) continue; const synthetic = name === "LifetimeRegion" @@ -1787,7 +1789,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // types-only package), so it maps structurally like a local declaration. // Values the package itself would produce stay refused at their import. const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => { - const decls = sym ? checker.declarationsOf(sym) : undefined; + const decls = sym ? sym.declarations : undefined; return decls && decls.length > 0 && decls.every((d) => { @@ -2001,7 +2003,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Class instances name a program class. The same symbol with construct // signatures names its static side, mapped to classval below. const widenedSym = widened.getSymbol(); - const classDecl = widenedSym ? checker.valueDeclarationOf(widenedSym) : undefined; + const classDecl = widenedSym ? widenedSym.valueDeclaration : undefined; if ( classDecl && (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) && @@ -2035,7 +2037,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; @@ -2047,6 +2049,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. @@ -2084,7 +2092,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); @@ -2123,9 +2131,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const psym = widened.getSymbol(); const isStdlibInterface = (name: string): boolean => psym?.name === name && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); // The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class // hierarchy (provenance, not the name — a user's own `class Error` // resolved through the class-instance branch above, and a user @@ -2139,9 +2147,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and in @types/node), not as a lib interface. if ( psym?.name === "AssertionError" && - checker - .declarationsOf(psym) - .some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) ) return { kind: "object", className: "%AssertionError" }; // The lib's `Object` interface is a TOP type: tsc admits every @@ -2159,16 +2165,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // included, while unrelated user interfaces named Module stay structural. if ( (psym?.name === "ScriptcModule" || psym?.name === "Module") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (psym.name === "ScriptcModule" || - isDeclaredInAmbientNamespace(d, "NodeJS") || - isDeclaredInAmbientModule(d, "module")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (psym.name === "ScriptcModule" || + isDeclaredInAmbientNamespace(d, "NodeJS") || + isDeclaredInAmbientModule(d, "module")), + ) ) { return F64; } @@ -2181,14 +2185,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // et al. extend) is the same runtime value. Provenance-checked. if ( psym?.name === "EventEmitter" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), + ) ) { return { kind: "object", className: RUNTIME_EMITTER_CLASS }; } @@ -2198,15 +2200,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // enclosing ambient module, like net.Server. if ( psym?.name === "Interface" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return F64; } @@ -2215,29 +2215,25 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // code member). if ( psym?.name === "Key" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "readline") || - isDeclaredInAmbientModule(d, "readline/promises")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "readline") || + isDeclaredInAmbientModule(d, "readline/promises")), + ) ) { return DYN; } // readline/promises.Readline: an f64 handle the same way. if ( psym?.name === "Readline" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "readline/promises"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "readline/promises"), + ) ) { return F64; } @@ -2249,14 +2245,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and @types/node's class Channel. if ( psym?.name === "Channel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } @@ -2265,14 +2259,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // process-lived; contexts ride the fiber machinery). if ( psym?.name === "AsyncLocalStorage" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "async_hooks"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "async_hooks"), + ) ) { return F64; } @@ -2280,21 +2272,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // runtime's tracing registry (five event channels per entry). if ( psym?.name === "TracingChannel" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "diagnostics_channel"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "diagnostics_channel"), + ) ) { return F64; } // Node names this result; the ambient WHATWG declaration uses a type literal. const encodeResult = psym && - checker.declarationsOf(psym).some((decl) => { + psym.declarations.some((decl) => { if (!ctx.isStdlibFile(decl.getSourceFile())) return false; if (ts.isInterfaceDeclaration(decl) && decl.name.text === "EncodeIntoResult") return true; const parent = decl.parent; @@ -2321,13 +2311,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Their private encoding slot is omitted from enumeration and JSON. if ( (psym?.name === "TextEncoder" || psym?.name === "TextDecoder") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2357,13 +2345,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record shape never surfaces. if ( psym?.name === "StringDecoder" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", @@ -2383,7 +2369,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record-holding binding forms compile. User-declared literals with // these member names keep the ordinary record path (their declarations // are not in declaration files). - const promiseDeclarations = psym ? checker.declarationsOf(psym) : []; + const promiseDeclarations = psym ? psym.declarations : []; if ( flags & ts.TypeFlags.Object && promiseDeclarations.length > 0 && @@ -2449,9 +2435,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "ReadableStreamReadResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = widened.getAliasTypeArguments()?.[0]; const elem = @@ -2464,9 +2450,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym && (psym.name === "ReadableStreamReadValueResult" || psym.name === "ReadableStreamReadDoneResult") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const elemTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const elem = @@ -2478,6 +2464,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // whose getters and methods dispatch through the handle. if ( (psym?.name === "SocketAddress" || psym?.name === "BlockList") && + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) { + return DYN; + } + // WebCrypto: the crypto global and its subtle object are dynamic + // objects, CryptoKey values are native handles (scr_webcrypto.c), and + // the algorithm dictionaries the methods take and keys report are plain + // checked-dynamic objects. + if ( + psym && + WEBCRYPTO_DYN_TYPES.has(psym.name) && checker .declarationsOf(psym) .some( @@ -2493,13 +2494,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // that export() returns is a plain checked-dynamic object. if ( (psym?.name === "KeyObject" || psym?.name === "JsonWebKey") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2508,17 +2507,15 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (FETCH_AMBIENT_TYPES as readonly string[]).includes(psym.name) && !( psym.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS")) - ) && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) + ) && + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2534,9 +2531,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "Disposable" || psym?.name === "AsyncDisposable" || psym?.name === "DisposableStack") && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return DYN; } @@ -2546,7 +2543,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && CIPHER_TYPE_NAMES.has(psym.name) && - checker.declarationsOf(psym).some((d) => ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) ) { return DYN; } @@ -2674,13 +2671,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the same symbol); provenance-checked like URL. if ( psym?.name === "Buffer" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return bytesOf("u8"); } @@ -2691,13 +2686,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record/class like any other. if ( psym?.name === "URL" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "url" }; } @@ -2706,13 +2699,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // URLSearchParams` in the "url" module). Provenance-checked like URL. if ( psym?.name === "URLSearchParams" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "searchParams" }; } @@ -2720,13 +2711,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declarations' interface. Provenance-checked like URL. if ( psym?.name === "Stats" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "stats" }; } @@ -2736,14 +2725,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "StatsFs" || psym?.name === "BigIntStatsFs" || psym?.name === "StatFsOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) return DYN; if (isWorkerHandleSymbol(psym, ctx)) return DYN; @@ -2752,14 +2739,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node declare it in "fs/promises". if ( psym?.name === "FileHandle" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs/promises"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs/promises"), + ) ) { return { kind: "fileHandle" }; } @@ -2767,14 +2752,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and @types/node. if ( psym?.name === "Dir" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "dir" }; } @@ -2784,13 +2767,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like Stats. if ( psym?.name === "SpawnSyncReturns" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "spawnRes" }; } @@ -2802,14 +2783,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym.name === "ChildProcess" || psym.name === "ChildProcessWithoutNullStreams" || psym.name === "ChildProcessByStdio") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + ) ) { return { kind: "child" }; } @@ -2822,7 +2801,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // site. if ( (psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") && - checker.declarationsOf(psym).some( + psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && ctx.isStdlibFile(d.getSourceFile()) && @@ -2848,14 +2827,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // "tls"`, and the shipped fallback mirrors it. if ( psym?.name === "SecureContext" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "tls"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "tls"), + ) ) { return { kind: "secureCtx" }; } @@ -2863,14 +2840,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked values so presence probes and repeated getter reads match Node. if ( psym?.name === "ScryptOptions" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) return DYN; // crypto.Hash / crypto.Hmac: native incremental digest handles. Keep @@ -2878,14 +2853,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // retain their ordinary structural/class representation. if ( (psym?.name === "Hash" || psym?.name === "Hmac") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return psym.name === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; } @@ -2895,14 +2868,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // mirrors it. if ( psym?.name === "FSWatcher" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "fsWatcher" }; } @@ -2925,25 +2896,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // scalar; the lowered surface is write(string). if ( (psym?.name === "WriteStream" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "tty") || - (isDeclaredInAmbientModule(d, "process") && - isDeclaredInAmbientNamespace(d, "NodeJS"))), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "tty") || + (isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))), + )) || (psym?.name === "WritableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return PROCSTREAM_T; } @@ -2953,24 +2919,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // of the broad @types class fence where they are constructed. if ( (psym?.name === "ChildStdin" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "child_process"), - )) || + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "child_process"), + )) || (psym?.name === "Writable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - )) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + )) ) { return { kind: "childWriter" }; } @@ -2984,15 +2946,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { if ( psym?.name === rec.lib && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "stream"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "stream"), + ) ) { return { kind: "object", className: irName }; } @@ -3002,15 +2962,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( psym !== undefined && ZLIB_STREAM_MODES.has(psym.name) && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "zlib"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "zlib"), + ) ) { return { kind: "object", className: "%Transform" }; } @@ -3019,37 +2977,31 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // (scr_fs_stream.c). if ( (psym?.name === "ReadStream" || psym?.name === "WriteStream") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - !isNodeTypesPath(d.getSourceFile().fileName) && - isDeclaredInAmbientModule(d, "fs"), - ) + psym.declarations.some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "fs"), + ) ) { return { kind: "object", className: psym.name === "ReadStream" ? "%Readable" : "%Writable" }; } if ( (psym?.name === "Readable" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "stream"), - )) || + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "stream"), + )) || (psym?.name === "ReadableStream" && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientNamespace(d, "NodeJS"), - )) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + )) ) { return { kind: "childStream" }; } @@ -3059,14 +3011,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // inside `declare module "dgram"`. if ( psym?.name === "Socket" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "dgram"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "dgram"), + ) ) { return { kind: "dgramSocket" }; } @@ -3078,14 +3028,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // both spellings). if ( (psym?.name === "TestContext" || psym?.name === "SuiteContext") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { return { kind: "testCtx" }; } @@ -3094,14 +3042,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // { %call: F, mock } record through mapHybridCallableIntersection. if ( (psym?.name === "MockFunctionContext" || psym?.name === "MockFunctionCall") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "test"), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "test"), + ) ) { const fnTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const fn = fnTs ? mapType(fnTs, ctx) : null; @@ -3117,9 +3063,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Provenance-checked like the rest. if ( psym?.name === "ErrnoException" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "object", className: "%Error" }; } @@ -3131,14 +3077,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Agent extends http's; both live in their ambient modules). if ( psym?.name === "Agent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), + ) ) { return { kind: "dyn" }; } @@ -3147,14 +3091,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // rule). if ( (psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; } @@ -3170,14 +3112,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "Http2SecureServer" || psym?.name === "Http2ServerRequest" || psym?.name === "Http2ServerResponse") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2SecureServer" ? { kind: "netServer" } @@ -3196,14 +3136,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ServerHttp2Session" || psym?.name === "ClientHttp2Stream" || psym?.name === "ServerHttp2Stream") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http2"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http2"), + ) ) { return psym.name === "Http2Server" ? { kind: "netServer" } @@ -3215,14 +3153,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // like IncomingMessage. if ( psym?.name === "ClientRequest" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "http"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "http"), + ) ) { return { kind: "httpClientReq" }; } @@ -3236,9 +3172,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // and interns the SAME shape. if ( psym?.name === "ProcessEnv" && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const value = withUndefinedArm(STRING, ctx.unions); if (!value) return null; @@ -3328,9 +3264,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = widened.getAliasSymbol(); if ( alias?.name === "IteratorResult" && - checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ) { const targs = widened.getAliasTypeArguments() ?? []; const channels = genChannels(targs[0], targs[1], undefined, ctx); @@ -3465,14 +3401,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "ReadOptions" || psym?.name === "ReadOptionsWithBuffer" || psym?.name === "WriteOptions") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), + ) ) { const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { @@ -3496,16 +3430,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym?.name === "FileWritevResult" || psym?.name === "ReadVResult" || psym?.name === "WriteVResult") && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "fs/promises") || - ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && - isDeclaredInAmbientModule(d, "fs"))), - ) + psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "fs/promises") || + ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && + isDeclaredInAmbientModule(d, "fs"))), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length !== 1) return null; @@ -3563,13 +3495,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // provenance-checked, the Buffer/URL pattern). if ( psym?.name === "Dirent" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length > 1) return null; @@ -3617,18 +3547,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }); if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ts.isPropertySignature(d.parent) && - ts.isIdentifier(d.parent.name) && - d.parent.name.text === "times" && - ts.isInterfaceDeclaration(d.parent.parent) && - d.parent.parent.name.text === "CpuInfo" && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ts.isPropertySignature(d.parent) && + ts.isIdentifier(d.parent.name) && + d.parent.name.text === "times" && + ts.isInterfaceDeclaration(d.parent.parent) && + d.parent.parent.name.text === "CpuInfo" && + ctx.isStdlibFile(d.getSourceFile()), + ) ) return cpuTimes(); if (isStdlibInterface("CpuInfo")) { @@ -3696,16 +3624,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // unmapped. if ( psym && - checker - .declarationsOf(psym) - .some( - (d) => - ts.isTypeLiteralNode(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (isDeclaredInAmbientModule(d, "child_process") || - isDeclaredInAmbientModule(d, "util") || - isDeclaredInAmbientModule(d, "dns")), - ) + psym.declarations.some( + (d) => + ts.isTypeLiteralNode(d) && + ctx.isStdlibFile(d.getSourceFile()) && + (isDeclaredInAmbientModule(d, "child_process") || + isDeclaredInAmbientModule(d, "util") || + isDeclaredInAmbientModule(d, "dns")), + ) ) { const props = checker.getPropertiesOfType(widened); const names = @@ -3745,14 +3671,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // internal-slot object shows {} — the Dirent %dtype edge's cousin. if ( psym?.name === "X509Certificate" && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isDeclaredInAmbientModule(d, "crypto"), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()) && + isDeclaredInAmbientModule(d, "crypto"), + ) ) { return { kind: "record", @@ -3780,13 +3704,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (psym?.name === "PerformanceEntry" || psym?.name === "PerformanceMark" || psym?.name === "PerformanceMeasure") && - checker - .declarationsOf(psym) - .some( - (d) => - (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), - ) + psym.declarations.some( + (d) => + (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) ) { return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) }; } @@ -4039,10 +3961,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { flags & ts.TypeFlags.Object && callSigs.length === 0 && psym !== undefined && - checker.declarationsOf(psym).length > 0 && - checker - .declarationsOf(psym) - .every((d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.length > 0 && + psym.declarations.every( + (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()), + ) ) { const CHILD_SURFACE = new Set([ "pid", @@ -4288,9 +4210,9 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // convention. Detected by the param-count mismatch against the // signature's own declaration, excluding an erased `this` parameter. { - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; 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 @@ -4308,7 +4230,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null let typedRest = false; let dynRest = false; for (const p of sig.getParameters()) { - const decl = checker.valueDeclarationOf(p); + const decl = p.valueDeclaration; const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined; // arity ABI selected by lambdaSignature. Treating an inferred any[] @@ -4340,13 +4262,11 @@ 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 && - checker - .declarationsOf(constructorSymbol) - .some((site) => ctx.isStdlibFile(site.getSourceFile())); + constructorSymbol.declarations.some((site) => ctx.isStdlibFile(site.getSourceFile())); if ( !nativeExecutor && decl && @@ -4400,7 +4320,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // like declaredReturnType does for declarations. JS null/undefined // returns can read mutable fields, so callable values use the same // checked-native return ABI as their implementations. - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && isJsSourceFile(sigDecl.getSourceFile()) && @@ -4541,7 +4461,7 @@ function mapClassView( bases = ts.constituentTypes(type); } else { const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4579,7 +4499,7 @@ function mapClassView( let representation: IrType | undefined; for (const base of bases) { const symbol = base.getSymbol(); - const declaration = symbol ? checker.valueDeclarationOf(symbol) : undefined; + const declaration = symbol ? symbol.valueDeclaration : undefined; const classBase = declaration && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && @@ -4603,8 +4523,8 @@ function mapClassView( const original = checker.getPropertyOfType(baseType, property.name); if (!original) return null; if (original === property) continue; - const originalDeclarations = checker.declarationsOf(original); - const declarations = checker.declarationsOf(property); + const originalDeclarations = original.declarations; + const declarations = property.declarations; // Instantiating an inherited member can create a new symbol without // changing its declaration or ABI. Only refinements need validation. if ( @@ -4642,7 +4562,7 @@ function mapCollectionView( if (!type.isTypeReference()) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const declarations = symbol ? checker.declarationsOf(symbol) : []; + const declarations = symbol ? symbol.declarations : []; if ( declarations.length === 0 || !declarations.every( @@ -4695,16 +4615,14 @@ function mapCollectionView( const native = baseSymbol && ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(baseSymbol.name) && - checker - .declarationsOf(baseSymbol) - .some((decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile())); + baseSymbol.declarations.some( + (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile()), + ); const mapped = native ? mapType(base, baseCtx) : mapCollectionView(base, baseCtx, seen); if (mapped?.kind !== "map" && mapped?.kind !== "set") return null; const hasType = funcOf([mapped.kind === "map" ? mapped.key : mapped.elem], BOOL); for (const property of checker.getPropertiesOfType(type)) { - const own = checker - .declarationsOf(property) - .filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); + const own = property.declarations.filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); if (own.length === 0) continue; if ( property.name !== "has" || @@ -4854,15 +4772,17 @@ export function underConstAssertion(n: ts.Node): boolean { * panic repair): a property symbol declared by a property assignment whose * initializer is an EMPTY array literal inside a const assertion — its * type is provably `readonly []`, whatever the panicked query answered. */ -function constAssertedEmptyArrayProp(p: ts.Symbol, ctx: TypeMapperCtx): boolean { - const d = ctx.checker.valueDeclarationOf(p); +function constAssertedEmptyArrayProp(p: ts.Symbol): boolean { + const d = p.valueDeclaration; if (d === undefined || !ts.isPropertyAssignment(d)) return false; 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); } @@ -4876,9 +4796,9 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound) return null; @@ -4942,7 +4862,7 @@ function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null if (!resolveTypeParamTs || !isMappedShape(widened)) return null; const symbol = widened.getSymbol(); const node = symbol - ? checker.declarationsOf(symbol).find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) + ? symbol.declarations.find((d): d is ts.MappedTypeNode => ts.isMappedTypeNode(d)) : undefined; const constraint = node?.typeParameter.constraint; if ( @@ -5028,14 +4948,15 @@ function evalMappedTemplate( const alias = type.getAliasSymbol(); const aliasArgs = type.getAliasTypeArguments(); const aliasDecl = alias - ? checker - .declarationsOf(alias) - .find((d): d is ts.TypeAliasDeclaration => ts.isTypeAliasDeclaration(d)) + ? alias.declarations.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); @@ -5085,9 +5006,7 @@ function matchInferReference( const want = wantArgs[i]!; const symbol = want.flags & ts.TypeFlags.TypeParameter ? want.getSymbol() : undefined; const isInfer = symbol - ? checker - .declarationsOf(symbol) - .some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) + ? symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent)) : false; if (isInfer && symbol) inferred.set(symbol, gotArgs[i]!); else if (want !== gotArgs[i]) return undefined; @@ -5121,9 +5040,9 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. - const isLibAlias = ctx.checker - .declarationsOf(alias) - .some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())); + const isLibAlias = alias.declarations.some( + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound || bound.kind !== "record") return null; @@ -5376,7 +5295,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType "AggregateError", "SuppressedError", ].includes(symbol.name) && - ctx.checker.declarationsOf(symbol).some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + symbol.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) ); }); if (!hasError) return null; @@ -5518,12 +5437,12 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null if (!isMappedShape(type)) return null; const { checker } = ctx; const symbol = type.getSymbol(); - const decl = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const decl = symbol ? symbol.declarations[0] : undefined; if (decl === undefined || !CONTRACT_TYPES_FILE.test(decl.getSourceFile().fileName)) return null; const props = checker.getPropertiesOfType(type); const seen = new Set(); for (const prop of props) { - const owner = checker.declarationsOf(prop)[0]?.parent; + const owner = prop.declarations[0]?.parent; if (owner === undefined || seen.has(owner)) continue; seen.add(owner); let source: ts.Type | null = null; @@ -5531,7 +5450,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); @@ -5590,21 +5509,19 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { } if (isMappedShape(t)) return true; const tSym = t.getSymbol(); - const decls = tSym ? checker.declarationsOf(tSym) : undefined; + const decls = tSym ? tSym.declarations : undefined; if (!decls || decls.length === 0) { // Inferred destructuring parameters can have a synthesized anonymous // shape with no declaration or member symbols to trace to a source. return checker .getPropertiesOfType(t) .every((property) => - checker - .declarationsOf(property) - .every( - (decl) => - !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()) || - packageTypeFile(decl.getSourceFile(), ctx), - ), + property.declarations.every( + (decl) => + !decl.getSourceFile().isDeclarationFile || + ctx.isExternalTypeFile(decl.getSourceFile()) || + packageTypeFile(decl.getSourceFile(), ctx), + ), ); } return !decls.some((d) => { @@ -5713,7 +5630,7 @@ export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeCheck ) return false; return sigs.every((sig) => { - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; if (decl === undefined) return false; if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) return false; @@ -5837,7 +5754,7 @@ function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { }; const mapped = mapCallSignature(sig, sigCtx); if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; - const decl = checker.signatureDeclaration(sig); + const decl = sig.declaration; const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; const which = spelled ? `overload '${spelled}'` : "one overload"; if (mapped?.kind === "func") @@ -6109,7 +6026,7 @@ function mapRecordTypeInner( // representable. const anonSym = widened.getSymbol(); const anonymousEmpty = - (anonSym === undefined || checker.declarationsOf(anonSym).length === 0) && + (anonSym === undefined || anonSym.declarations.length === 0) && !widened.isIntersectionType() && indexValue === undefined && checker.getPropertiesOfType(widened).length === 0; @@ -6148,10 +6065,7 @@ function mapRecordTypeInner( indexValue === undefined && checker.getCallSignatures(widened).length === 0 && checker.getConstructSignatures(widened).length === 0 && - !( - anonSym !== undefined && - checker.declarationsOf(anonSym).some((d) => ts.isObjectLiteralExpression(d)) - ) + !(anonSym !== undefined && anonSym.declarations.some((d) => ts.isObjectLiteralExpression(d))) ) { return DYN; } @@ -6172,7 +6086,7 @@ function mapRecordTypeInner( // their existing paths (the CJS export-table narrowing and the // checked-dynamic fallback), and .d.ts declarations stay out with // the rest of the lib's type worlds. - const decls = checker.declarationsOf(p); + const decls = p.declarations; const getDecl = decls.find((d) => ts.isGetAccessorDeclaration(d)); const setDecl = decls.find((d) => ts.isSetAccessorDeclaration(d)); const accessorOwned = @@ -6202,7 +6116,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 @@ -6222,7 +6136,7 @@ function mapRecordTypeInner( // declarations and pass. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) @@ -6253,7 +6167,7 @@ function mapRecordTypeInner( if ( pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0 && - constAssertedEmptyArrayProp(p, ctx) + constAssertedEmptyArrayProp(p) ) { pt = arrayOf(unitOnlyUnion(ctx.unions)); } @@ -6422,9 +6336,9 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): const container = psym !== undefined && Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, psym.name) && - checker - .declarationsOf(psym) - .some((d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym.declarations.some( + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + ) ? psym.name : undefined; if (container !== undefined) { @@ -6555,11 +6469,11 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): if (callSigs.length === 1) { const sig = callSigs[0]!; if (sig.getTypeParameters().length > 0) return null; - const sigDecl = checker.signatureDeclaration(sig); + const sigDecl = sig.declaration; 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`; @@ -6605,7 +6519,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx // any one member's type — that story stays with the residual fence. if ( computed && - checker.declarationsOf(p).some((d) => { + p.declarations.some((d) => { const sf = d.getSourceFile(); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) 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..99c97dded 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -17,15 +17,15 @@ 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(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; return ( declaration !== undefined && ts.isVariableDeclaration(declaration) && @@ -53,7 +53,7 @@ export function isWorkerConstructor(program: ts.Program, expression: ts.Expressi if (isBuiltinMemberImport(program, expression, "worker_threads", "Worker")) return true; const checker = program.getTypeChecker(), symbol = checker.getSymbolAtLocation(expression); - const declaration = symbol ? checker.declarationsOf(symbol)[0] : undefined; + const declaration = symbol ? symbol.declarations[0] : undefined; if ( declaration && ts.isBindingElement(declaration) && @@ -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 56b787e18..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,10 +549,12 @@ 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, cipher: features.cipher, + webcrypto: features.webcrypto, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), ...(ffi === null ? {} @@ -1046,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 fc4c383ff..20dc82555 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -137,6 +137,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "fetch.streamNew", "fetch.streamFrom", "num.toFixed", + "num.toPrecision", + "num.toExponentialDigits", + "str.localeCase", "num.toStringRadix", "num.toStringRadixF64", "insp.jsonDyn", @@ -569,6 +572,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.propertyIsEnumerableComputed", // the destructuring pack throws V8's TypeError on non-iterable dyn kinds "dyn.iterPack", + "dyn.destructureRest", "dyn.arrayFromIterator", "dyn.iterator", "dyn.iteratorResult", @@ -677,6 +681,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.randomIntMax", "crypto.randomFillView", "crypto.randomFillViewRest", + "crypto.getRandomValues", + "crypto.getRandomValuesDyn", "crypto.randomFillSyncDyn", "crypto.randomFillCb", "crypto.randomFillCbDyn", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 51683990b..102521a09 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -176,12 +176,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * not iterable (cannot read property Symbol(Symbol.iterator))"). In the * may-throw seed set. */ "dyn.iterPack": { argTypes: [DYN, STRING], result: DYN }, + "dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN }, "dyn.arrayFromIterator": { argTypes: [DYN], result: DYN }, "dyn.iterator": { argTypes: [DYN, STRING], result: DYN }, "dyn.iteratorResult": { argTypes: [DYN], result: DYN }, "dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL }, "dyn.iteratorStep": { argTypes: [DYN], result: DYN }, "dyn.iteratorStepDone": { argTypes: [DYN], result: BOOL }, + "dyn.setGeneratorMembers": { argTypes: [DYN, DYN], result: VOID }, + "dyn.iteratorHelperMethod": { argTypes: [STRING], result: DYN }, + "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 @@ -564,6 +568,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** os.constants: the per-thread null-prototype constants object (dlopen, * errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */ "os.constants": { argTypes: [], result: DYN }, + /** fs.constants: the target's cached null-prototype constants object. */ + "fs.constants": { argTypes: [], result: DYN }, /** The deprecated node:constants object (scr_os.c): the os groups and * fs.constants merged with the borrowed baked crypto.constants table, * frozen, one per thread. */ @@ -691,6 +697,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for * Latin-script text (scr_str_locale_compare). Never throws. */ "str.localeCompare": { argTypes: [STRING, STRING], result: F64 }, + /** Annex B substr(start, length) with start ToNumber'd and length's + * presence; never throws. */ + "str.substr": { argTypes: [STRING, F64, F64, BOOL], result: STRING }, + /** toLocaleLowerCase/toLocaleUpperCase (scr_regex.c): the locales value + * as written, upper=true for toLocaleUpperCase. Throws for null and for + * locale forms with no native rules. */ + "str.localeCase": { argTypes: [STRING, DYN, BOOL], result: STRING }, "str.replaceLiteral": { argTypes: [STRING, STRING, STRING, BOOL], result: STRING }, // (subject, characters, NUL-separated replacement texts): the fused // chain of global one-character replacements. @@ -755,6 +768,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "num.toExponential": { argTypes: [F64], result: STRING }, "num.toFixed0": { argTypes: [F64], result: STRING }, "num.toFixed": { argTypes: [F64, F64], result: STRING }, + /** toPrecision(precision) with the argument ToNumber'd and its presence + * (undefined answers ToString); toExponential with a DEFINED + * fractionDigits. Both throw V8's RangeError for out-of-range digits. */ + "num.toPrecision": { argTypes: [F64, F64, BOOL], result: STRING }, + "num.toExponentialDigits": { argTypes: [F64, F64], result: STRING }, "num.toStringRadix": { argTypes: [F64, DYN], result: STRING }, /** toString(radix) with a number radix (scr_lib.c): truncates it as * ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */ @@ -1478,6 +1496,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 }, @@ -2597,6 +2619,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * whether min was given. */ "crypto.randomFillView": { argTypes: [null, F64, F64], result: BYTES_U8 }, "crypto.randomFillViewRest": { argTypes: [null, F64], result: BYTES_U8 }, + /** crypto.getRandomValues: a view returns itself at its own type (the + * float/size DOMExceptions are thrown at run time); any other value + * takes the checked form. */ + "crypto.getRandomValues": { argTypes: [null], result: BYTES_U8 }, + "crypto.getRandomValuesDyn": { argTypes: [DYN], result: DYN }, "crypto.randomFillSyncDyn": { argTypes: [DYN, DYN, DYN], result: DYN }, "crypto.randomFillCb": { argTypes: [null, F64, F64, F64, null], result: VOID }, "crypto.randomFillCbDyn": { argTypes: [DYN, DYN, DYN, F64, null], result: VOID }, @@ -2609,6 +2636,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as + * one dyn array and answers a promise that argument and validation + * failures reject. CryptoKey values are native handles. */ + "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, + "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, @@ -2647,6 +2682,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.fromStr": { argTypes: [STRING, STRING], result: BYTES_U8 }, "buffer.fromStrChecked": { argTypes: [STRING, STRING], result: BYTES_U8 }, "buffer.brand": { argTypes: [BYTES_U8], result: BYTES_U8 }, + /** Buffer.isBuffer over a typed view: whether the view carries the + * Buffer brand (any element kind; only Buffers do). Never throws. */ + "buffer.isBranded": { argTypes: [null], result: BOOL }, /** Buffer.from on checked-native strings, bytes, arrays and data-only * array-like/Buffer-JSON objects. The encoding is a normalized literal; * non-string inputs ignore it. Copies the input and may throw during @@ -3409,6 +3447,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 @@ -3887,7 +3927,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 }, @@ -4221,6 +4261,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 ab4b05c8b..117bba24a 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -143,6 +143,7 @@ const FUNCTION_FIELDS: Record>, Role> = { "captures[].localId": "other", "classCaptures[].name": "other", "classCaptures[].localId": "other", + "generator.tag": "other", }; const MODULE_FIELDS: Record>, Role> = { diff --git a/packages/compiler/src/ir/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 0aa510b8d..67622719c 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1494,6 +1494,27 @@ export const RUNTIME_ERROR_CLASSES: ReadonlyMap< * owns listener closures, whatever the subclass fields say. */ export const RUNTIME_EMITTER_CLASS = "%EventEmitter"; +/** The function that construction through a class value calls, and the + * index of its first argument. A program class constructs through + * `%C.constructor(this, ...)` after the thunk allocates; a runtime-provided + * class (EventEmitter, the stream and error classes) through the + * frontend's `%C.construct(...)`, which allocates through the runtime and + * returns the new instance. */ +export function classValueConstructor(def: { name: string; runtime?: true }): { + fn: string; + firstArg: number; +} { + return def.runtime + ? { fn: `%${def.name}.construct`, firstArg: 0 } + : { fn: `%${def.name}.constructor`, firstArg: 1 }; +} + +/** The JavaScript `.name` of a class: its jsName, or the runtime-provided + * class's own name (`%TypeError` is TypeError). */ +export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string { + return def.jsName ?? (def.runtime ? def.name.replace(/^%/, "") : ""); +} + /** The runtime-provided node:stream classes (ScrStream / scr_stream_*, * scr_stream.c — link-gated by moduleUsesStream). They root at the * emitter class (base chains below), so the emitter method surface and @@ -1712,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; @@ -2431,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 @@ -2553,18 +2591,8 @@ export type IrExpr = | { kind: "logical"; op: "&&" | "||"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strConcat"; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "strEq"; negated: boolean; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } - /** String ordering. Ordinary source comparisons omit `utf16` and retain - * scriptc's documented code-point order; the default Array sort comparator - * sets it to request ECMAScript's UTF-16 code-unit order. */ - | { - kind: "strCmp"; - op: IrStrCmpOp; - left: IrExpr; - right: IrExpr; - utf16?: boolean; - type: IrType; - loc: SrcLoc; - } + /** String ordering in ECMAScript's UTF-16 code-unit order. */ + | { kind: "strCmp"; op: IrStrCmpOp; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } /** f64|bool → string, JS-exact (Number::toString / "true"/"false"). * Union operands dispatch through the per-union ToString helper (arms * fenced to unit/string/f64/bool by the frontend); a CAUGHT operand is diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 04443ee18..8d2d6bc63 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -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; @@ -43,6 +49,8 @@ export interface RuntimeFeatures { tls: boolean; tlsCa: boolean; cipher: boolean; + /** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */ + webcrypto: boolean; } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ @@ -108,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", @@ -123,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). */ @@ -208,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,6 +409,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -239,127 +422,33 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run const expr = (node: IrExpr): boolean => { if (node.kind === "libCall") { const fn = node.fn; - if ( - fn === "regexp.escape" || - fn === "dyn.nativeRegexIs" || - fn === "util.stripVTControlCharacters" || - fn === "path.matchesGlob" || - fn === "path.win32MatchesGlob" || - // fs.glob's walk compiles its patterns with the glob matcher. - fn === "fs.globSync" || - fn === "fs.globTypesSync" || - fn === "fs.callbackGlob" || - fn === "fsp.globStrings" || - fn === "fsp.globTypes" || - fn === "fsp.globError" - ) - features.regex = true; - if ( - fn === "text.decodeLegacy" || - fn === "text.decodeLegacyOptions" || - fn === "text.decodeStream" - ) - features.legacyTextDecoder = true; + if (REGEX_LIB_FNS.has(fn)) features.regex = true; + if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true; if (fn.startsWith("fetch.")) features.fetch = true; if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true; - // Signal-taking readline, stream and timers/promises entry points - // register abort listeners. - if ( - fn === "rl.createSignal" || - fn === "rl.questionAsyncSignal" || - fn === "http.clientSignal" || - fn === "tp.setTimeoutSignal" || - fn === "tp.schedulerWaitSignal" || - fn === "tp.setImmediateSignal" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn === "sp.finishedOptsSignal" || - fn === "sp.pipelineOptsSignal" || - fn === "stream.finishedOptsSignal" - ) { - features.eventTarget = true; - } + if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true; if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true; - // readline's terminal unit (scr_readline_tty.c) measures with the - // regex unit's stripVTControlCharacters; with a completer it prints - // completion errors through the inspect unit. - if ( - fn === "rl.cursorPos" || - fn === "rl.keypressStdin" || - fn === "rl.keypressStream" || - fn === "stdin.onKeypress" - ) - features.regex = true; + // With a completer, readline's terminal unit prints completion errors + // through the inspect unit. if (fn === "rl.terminal") { features.regex = true; if (!isDynUndefined(node.args[3]!)) features.inspect = true; } if (fn.startsWith("emitter.")) features.emitter = true; - if ( - fn.startsWith("readable.") || - fn.startsWith("writable.") || - fn.startsWith("duplex.") || - fn.startsWith("transform.") || - fn.startsWith("passthrough.") || - fn.startsWith("sc.") || - fn.startsWith("stream.set") || - fn === "stream.destroy" || - fn === "stream.destroyErr" || - fn === "stream.prop" || - fn === "stream.workerStdio" || - fn === "stream.errored" || - fn === "sp.finished" || - fn === "sp.pipeline" || - fn === "sp.pipelineStages" || - fn.startsWith("stream.pipelineStages") || - fn === "console.bind" || - fn === "crypto.hashStream" || - fn === "crypto.hashStreamPiped" || - fn === "stream.addAbortSignal" || - fn === "stream.addAbortSignalFn" || - fn.startsWith("sp.finishedOpts") || - fn.startsWith("sp.pipelineOpts") || - fn.startsWith("stream.finishedOpts") || - fn.startsWith("zlib.stream") || - fn === "fs.createReadStream" || - fn === "fs.createWriteStream" || - fn === "fileHandle.createReadStream" || - fn === "fileHandle.createWriteStream" || - fn.startsWith("fs.fsStream") - ) { + if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) { // the stream unit emits through the emitter unit, also when the // program constructs no stream (a pipeline of functions) features.stream = true; features.emitter = true; } if (fn.startsWith("zlib.")) features.zlib = true; - // The Readable operators and Readable.from over a compiled generator - // or a checked-dynamic iterator (scr_stream_ops.c) await through the - // dyn-async unit. - if ( - fn.startsWith("readable.op") || - fn.startsWith("readable.fromGen") || - fn === "readable.fromDyn" || - fn === "readable.fromIter" || - fn === "readable.fromAsyncIter" || - fn === "readable.fromAsyncGen" || - fn === "readable.fromDone" - ) + if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES)) features.dynAsync = true; if (fn.startsWith("dc.")) features.dc = true; if (fn.startsWith("assert.")) features.assert = true; - if ( - fn === "dyn.defineProps" || - fn === "dyn.definePrototypeProps" || - fn === "dyn.defineProperty" || - fn === "dyn.objCreateWithProperties" || - fn === "dyn.arrayProtoCall" || - fn === "dyn.arrayPrototype" || - fn === "dyn.functionApply" || - fn === "dyn.builtinMethod" - ) - features.dynInvoke = true; + if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true; if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true; + if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true; if ( fn.startsWith("insp.") || fn.startsWith("console.") || @@ -367,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.")) { @@ -392,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. @@ -412,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. @@ -435,6 +505,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run } if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; if (fn.startsWith("tlsca.")) features.tlsCa = true; + // Locale case mapping runs the default conversion of the regex unit. + if (fn === "str.localeCase") features.regex = true; // scr_cipher.c over the mbedTLS archive; DEP0182 reports through the // process-warning unit, and unknown output encodings render through // the inspect unit. @@ -443,6 +515,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.dynAsync = true; features.inspect = true; } + // crypto.subtle and CryptoKey: scr_webcrypto.c. + if (fn.startsWith("webcrypto.")) features.webcrypto = true; } else { switch (node.kind) { case "regexLit": @@ -450,30 +524,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/traverse.ts b/packages/compiler/src/ir/traverse.ts index 1ef70bdb2..6375eb39b 100644 --- a/packages/compiler/src/ir/traverse.ts +++ b/packages/compiler/src/ir/traverse.ts @@ -127,21 +127,54 @@ export function everyExprChild( expr: (expr: IrExpr) => boolean, stmt: (stmt: IrStmt) => boolean, ): boolean { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "numLit": + case "varRef": return true; + case "call": + return node.args.every((child) => expr(child)); + case "dynFrom": + return expr(node.value); case "strLit": return true; - case "moduleNsRef": + case "libCall": + return node.args.every((child) => expr(child)); + case "dynCheck": + return expr(node.value); + case "strEq": + return expr(node.left) && expr(node.right); + case "fieldGet": + return expr(node.obj); + case "ternary": + return expr(node.cond) && expr(node.then) && expr(node.else_); + case "closure": return true; case "boolLit": return true; + case "numLit": + return true; case "unitLit": return true; - case "varRef": - return true; + case "seqExpr": + return node.stmts.every((child) => stmt(child)) && expr(node.result); + case "dynCall": + return ( + expr(node.callee) && + (node.receiver === undefined || expr(node.receiver)) && + (node.calleeNameValue === undefined || expr(node.calleeNameValue)) && + node.args.every((child) => expr(child)) + ); + case "dynTest": + return expr(node.value); + case "dynScalarEq": + return expr(node.left) && expr(node.right); + case "upcast": + return expr(node.value); case "bin": return expr(node.left) && expr(node.right); + case "moduleNsRef": + return true; case "unary": return expr(node.operand); case "incDec": @@ -156,14 +189,10 @@ export function everyExprChild( return expr(node.left) && expr(node.right); case "strConcat": return expr(node.left) && expr(node.right); - case "strEq": - return expr(node.left) && expr(node.right); case "strCmp": return expr(node.left) && expr(node.right); case "toString": return expr(node.operand); - case "ternary": - return expr(node.cond) && expr(node.then) && expr(node.else_); case "nullish": return expr(node.left) && expr(node.right); case "orDefault": @@ -206,12 +235,8 @@ export function everyExprChild( return node.seed === undefined || expr(node.seed); case "setIntrinsic": return expr(node.receiver) && node.args.every((child) => expr(child)); - case "call": - return node.args.every((child) => expr(child)); case "ffiCall": return node.args.every((child) => expr(child)); - case "closure": - return true; case "callValue": return ( expr(node.callee) && @@ -240,8 +265,6 @@ export function everyExprChild( return expr(node.callee) && node.args.every((child) => expr(child)); case "instanceOfValue": return expr(node.value) && expr(node.classValue); - case "upcast": - return expr(node.value); case "promiseVoidWiden": return expr(node.value); case "downcast": @@ -250,8 +273,6 @@ export function everyExprChild( return expr(node.value); case "virtualCall": return node.args.every((child) => expr(child)); - case "fieldGet": - return expr(node.obj); case "recordLit": return node.fields.every((child) => expr(child.value)); case "recordClone": @@ -260,15 +281,6 @@ export function everyExprChild( return expr(node.obj); case "recordKeyGet": return expr(node.obj) && expr(node.key); - case "dynFrom": - return expr(node.value); - case "dynCall": - return ( - expr(node.callee) && - (node.receiver === undefined || expr(node.receiver)) && - (node.calleeNameValue === undefined || expr(node.calleeNameValue)) && - node.args.every((child) => expr(child)) - ); case "dynInvoke": return ( expr(node.recv) && @@ -282,16 +294,10 @@ export function everyExprChild( node.fields === undefined || node.fields.every((child) => expr(child.key) && expr(child.value)) ); - case "dynTest": - return expr(node.value); case "dynKeyGet": return expr(node.key) && expr(node.value); case "dynHasKey": return expr(node.value); - case "dynScalarEq": - return expr(node.left) && expr(node.right); - case "seqExpr": - return node.stmts.every((child) => stmt(child)) && expr(node.result); case "dynDestrCheck": return expr(node.value); case "dynIterN": @@ -332,12 +338,8 @@ export function everyExprChild( return expr(node.left) && expr(node.right); case "intrinsic": return node.args.every((child) => expr(child)); - case "libCall": - return node.args.every((child) => expr(child)); case "jsonStringify": return expr(node.value); - case "dynCheck": - return expr(node.value); } node satisfies never; throw new Error("unhandled IR node"); @@ -349,19 +351,27 @@ export function everyStmtChild( expr: (expr: IrExpr) => boolean, stmt: (stmt: IrStmt) => boolean, ): boolean { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "varDecl": - return node.init === null || expr(node.init); - case "assign": - return expr(node.value); - case "exprStmt": - return expr(node.expr); + case "return": + return node.value === null || expr(node.value); case "if": return ( expr(node.cond) && node.then.every((child) => stmt(child)) && (node.else_ === null || node.else_.every((child) => stmt(child))) ); + case "varDecl": + return node.init === null || expr(node.init); + case "exprStmt": + return expr(node.expr); + case "assign": + return expr(node.value); + case "block": + return node.body.every((child) => stmt(child)); + case "fieldSet": + return expr(node.obj) && expr(node.value); case "while": return expr(node.cond) && node.body.every((child) => stmt(child)); case "doWhile": @@ -393,10 +403,6 @@ export function everyStmtChild( return expr(node.arr) && expr(node.index) && expr(node.value); case "forOf": return expr(node.iterable) && node.body.every((child) => stmt(child)); - case "return": - return node.value === null || expr(node.value); - case "fieldSet": - return expr(node.obj) && expr(node.value); case "recordSet": return expr(node.obj) && expr(node.value); case "recordKeySet": @@ -407,8 +413,6 @@ export function everyStmtChild( return true; case "continue": return true; - case "block": - return node.body.every((child) => stmt(child)); case "throw": return expr(node.value); case "runtimeFence": @@ -432,21 +436,54 @@ export function mapExprChildren( expr: (expr: IrExpr) => IrExpr, stmt: (stmt: IrStmt) => IrStmt, ): IrExpr { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "numLit": + case "varRef": return node; + case "call": + return { ...node, args: node.args.map((child) => expr(child)) }; + case "dynFrom": + return { ...node, value: expr(node.value) }; case "strLit": return node; - case "moduleNsRef": + case "libCall": + return { ...node, args: node.args.map((child) => expr(child)) }; + case "dynCheck": + return { ...node, value: expr(node.value) }; + case "strEq": + return { ...node, left: expr(node.left), right: expr(node.right) }; + case "fieldGet": + return { ...node, obj: expr(node.obj) }; + case "ternary": + return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) }; + case "closure": return node; case "boolLit": return node; + case "numLit": + return node; case "unitLit": return node; - case "varRef": - return node; + case "seqExpr": + return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) }; + case "dynCall": { + const result: Extract = { ...node, callee: expr(node.callee) }; + if (node.receiver !== undefined) result.receiver = expr(node.receiver); + if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); + result.args = node.args.map((child) => expr(child)); + return result; + } + case "dynTest": + return { ...node, value: expr(node.value) }; + case "dynScalarEq": + return { ...node, left: expr(node.left), right: expr(node.right) }; + case "upcast": + return { ...node, value: expr(node.value) }; case "bin": return { ...node, left: expr(node.left), right: expr(node.right) }; + case "moduleNsRef": + return node; case "unary": return { ...node, operand: expr(node.operand) }; case "incDec": @@ -461,14 +498,10 @@ export function mapExprChildren( return { ...node, left: expr(node.left), right: expr(node.right) }; case "strConcat": return { ...node, left: expr(node.left), right: expr(node.right) }; - case "strEq": - return { ...node, left: expr(node.left), right: expr(node.right) }; case "strCmp": return { ...node, left: expr(node.left), right: expr(node.right) }; case "toString": return { ...node, operand: expr(node.operand) }; - case "ternary": - return { ...node, cond: expr(node.cond), then: expr(node.then), else_: expr(node.else_) }; case "nullish": return { ...node, left: expr(node.left), right: expr(node.right) }; case "orDefault": @@ -542,12 +575,8 @@ export function mapExprChildren( receiver: expr(node.receiver), args: node.args.map((child) => expr(child)), }; - case "call": - return { ...node, args: node.args.map((child) => expr(child)) }; case "ffiCall": return { ...node, args: node.args.map((child) => expr(child)) }; - case "closure": - return node; case "callValue": { const callee = expr(node.callee); if (node.receiver === undefined) @@ -581,8 +610,6 @@ export function mapExprChildren( return { ...node, callee: expr(node.callee), args: node.args.map((child) => expr(child)) }; case "instanceOfValue": return { ...node, value: expr(node.value), classValue: expr(node.classValue) }; - case "upcast": - return { ...node, value: expr(node.value) }; case "promiseVoidWiden": return { ...node, value: expr(node.value) }; case "downcast": @@ -591,8 +618,6 @@ export function mapExprChildren( return { ...node, value: expr(node.value) }; case "virtualCall": return { ...node, args: node.args.map((child) => expr(child)) }; - case "fieldGet": - return { ...node, obj: expr(node.obj) }; case "recordLit": return { ...node, @@ -608,15 +633,6 @@ export function mapExprChildren( return { ...node, obj: expr(node.obj) }; case "recordKeyGet": return { ...node, obj: expr(node.obj), key: expr(node.key) }; - case "dynFrom": - return { ...node, value: expr(node.value) }; - case "dynCall": { - const result: Extract = { ...node, callee: expr(node.callee) }; - if (node.receiver !== undefined) result.receiver = expr(node.receiver); - if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); - result.args = node.args.map((child) => expr(child)); - return result; - } case "dynInvoke": { const result: Extract = { ...node, recv: expr(node.recv) }; if (node.calleeNameValue !== undefined) result.calleeNameValue = expr(node.calleeNameValue); @@ -636,16 +652,10 @@ export function mapExprChildren( value: expr(child.value), })), }; - case "dynTest": - return { ...node, value: expr(node.value) }; case "dynKeyGet": return { ...node, key: expr(node.key), value: expr(node.value) }; case "dynHasKey": return { ...node, value: expr(node.value) }; - case "dynScalarEq": - return { ...node, left: expr(node.left), right: expr(node.right) }; - case "seqExpr": - return { ...node, stmts: node.stmts.map((child) => stmt(child)), result: expr(node.result) }; case "dynDestrCheck": return { ...node, value: expr(node.value) }; case "dynIterN": @@ -686,12 +696,8 @@ export function mapExprChildren( return { ...node, left: expr(node.left), right: expr(node.right) }; case "intrinsic": return { ...node, args: node.args.map((child) => expr(child)) }; - case "libCall": - return { ...node, args: node.args.map((child) => expr(child)) }; case "jsonStringify": return { ...node, value: expr(node.value) }; - case "dynCheck": - return { ...node, value: expr(node.value) }; } node satisfies never; throw new Error("unhandled IR node"); @@ -703,13 +709,11 @@ export function mapStmtChildren( expr: (expr: IrExpr) => IrExpr, stmt: (stmt: IrStmt) => IrStmt, ): IrStmt { + // The most common node kinds come first: the engine tests string cases + // one after another, in source order. switch (node.kind) { - case "varDecl": - return { ...node, init: node.init === null ? null : expr(node.init) }; - case "assign": - return { ...node, value: expr(node.value) }; - case "exprStmt": - return { ...node, expr: expr(node.expr) }; + case "return": + return { ...node, value: node.value === null ? null : expr(node.value) }; case "if": return { ...node, @@ -717,6 +721,16 @@ export function mapStmtChildren( then: node.then.map((child) => stmt(child)), else_: node.else_ === null ? null : node.else_.map((child) => stmt(child)), }; + case "varDecl": + return { ...node, init: node.init === null ? null : expr(node.init) }; + case "exprStmt": + return { ...node, expr: expr(node.expr) }; + case "assign": + return { ...node, value: expr(node.value) }; + case "block": + return { ...node, body: node.body.map((child) => stmt(child)) }; + case "fieldSet": + return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "while": return { ...node, cond: expr(node.cond), body: node.body.map((child) => stmt(child)) }; case "doWhile": @@ -755,10 +769,6 @@ export function mapStmtChildren( iterable: expr(node.iterable), body: node.body.map((child) => stmt(child)), }; - case "return": - return { ...node, value: node.value === null ? null : expr(node.value) }; - case "fieldSet": - return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "recordSet": return { ...node, obj: expr(node.obj), value: expr(node.value) }; case "recordKeySet": @@ -769,8 +779,6 @@ export function mapStmtChildren( return node; case "continue": return node; - case "block": - return { ...node, body: node.body.map((child) => stmt(child)) }; case "throw": return { ...node, value: expr(node.value) }; case "runtimeFence": diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 44b6814db..fa3b693a3 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -56,7 +56,7 @@ import { maybePromiseUnionArmsOk, VOID, } from "./ir.js"; -import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js"; +import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js"; import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; @@ -2937,15 +2937,21 @@ function validateFunction( err(`upcast: "${sub}" does not extend "${sup}"`, e.loc); break; } - const subCtor = functions.get(`%${sub}.constructor`); - const supCtor = functions.get(`%${sup}.constructor`); - if (!subCtor || !supCtor) { + const subClass = classes.get(sub); + const supClass = classes.get(sup); + const subValueCtor = subClass ? classValueConstructor(subClass) : null; + const supValueCtor = supClass ? classValueConstructor(supClass) : null; + const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined; + const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined; + if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) { err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc); break; } + const subArgs = subCtor.params.slice(subValueCtor.firstArg); + const supArgs = supCtor.params.slice(supValueCtor.firstArg); const abiEqual = - subCtor.params.length === supCtor.params.length && - subCtor.params.every((p, i) => i === 0 || typeEquals(p.type, supCtor.params[i]!.type)); + subArgs.length === supArgs.length && + subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type)); if (!abiEqual) { err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc); } @@ -2970,16 +2976,13 @@ function validateFunction( err(`classRef to undeclared class "${e.className}"`, e.loc); break; } - if (cls.runtime) { - err(`classRef to runtime-provided class "${e.className}"`, e.loc); - } - if (cls.jsName === undefined) { + if (cls.jsName === undefined && !cls.runtime) { err(`classRef to "${e.className}" without a jsName (the class object's .name)`, e.loc); } // A class value can always be constructed through — the frontend // notes the constructor edge at every classRef, so the emitted // thunk has a body to call. - if (!functions.has(`%${e.className}.constructor`)) { + if (!functions.has(classValueConstructor(cls).fn)) { err(`classRef to "${e.className}" without its constructor function`, e.loc); } if (!typeEquals(e.type, { kind: "classval", className: e.className })) { @@ -3008,21 +3011,24 @@ function validateFunction( break; } const cls = e.callee.type.className; - const ctor = functions.get(`%${cls}.constructor`); - if (!ctor) { + const calleeClass = classes.get(cls); + const valueCtor = calleeClass ? classValueConstructor(calleeClass) : null; + const ctor = valueCtor ? functions.get(valueCtor.fn) : undefined; + if (!ctor || !valueCtor) { err(`newValue on "${cls}": missing constructor function`, e.loc); break; } // Count-exact against the static class's completed ABI (every // value legally in the slot shares it — the upcast rule). - if (ctor.params.length !== e.args.length + 1) { + const first = valueCtor.firstArg; + if (ctor.params.length !== e.args.length + first) { err( - `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - 1}`, + `newValue on "${cls}": ${e.args.length} args, ctor expects ${ctor.params.length - first}`, e.loc, ); } e.args.forEach((a, i) => { - const p = ctor.params[i + 1]; + const p = ctor.params[i + first]; if (p) expectType(a, p.type, `newValue on "${cls}" arg ${i}`); }); if (!typeEquals(e.type, { kind: "object", className: cls })) { @@ -5236,7 +5242,11 @@ function validateFunction( } function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean { - if (e.fn === "crypto.randomFillView" || e.fn === "crypto.randomFillViewRest") { + if ( + e.fn === "crypto.randomFillView" || + e.fn === "crypto.randomFillViewRest" || + e.fn === "crypto.getRandomValues" + ) { // The filled view is the result, at its own element type. const view = e.args[0]?.type; if (view?.kind !== "bytes" || !typeEquals(e.type, view)) { diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 3c3855e5c..2d1bc85d4 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -51,6 +51,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["dns.", "the node:dns surface"], ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], + ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], ["test.", "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index c0d89bbbf..7d00acbc8 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -100,6 +100,7 @@ async function main(): Promise { nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, options.optimization ?? "release", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 18635c8c7..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", @@ -153,6 +155,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tls", "tlsCa", "cipher", + "webcrypto", ].every((name) => typeof features[name] === "boolean"); } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 9047f3376..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, @@ -103,6 +104,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tls: false, tlsCa: false, cipher: false, + webcrypto: false, nativeFetch: false, netEffective: false, httpEffective: 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 350a81a2d..01d12dedc 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -618,6 +618,12 @@ "code": "SC2020", "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" }, + { + "id": "node-builtin.crypto.getRandomValues", + "kind": "node-builtin", + "name": "crypto.getRandomValues", + "status": "static" + }, { "id": "node-builtin.crypto.hash", "kind": "node-builtin", @@ -868,6 +874,13 @@ "name": "fs.closeSync", "status": "static" }, + { + "id": "node-builtin.fs.constants", + "kind": "node-builtin", + "name": "fs.constants", + "status": "static", + "note": "node_constants.cc's fs group under the target libc's macros; members a platform lacks read as undefined" + }, { "id": "node-builtin.fs.copyFileSync", "kind": "node-builtin", @@ -4209,6 +4222,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 +4235,19 @@ "status": "static", "note": "UTC ISO formatting over constructed and stored Date values" }, + { + "id": "stdlib.date.toString", + "kind": "stdlib", + "name": "Date.prototype.toString", + "status": "static", + "note": "local time with the host zone's long English name; String(date) and template literals use it" + }, + { + "id": "stdlib.date.toTimeString", + "kind": "stdlib", + "name": "Date.prototype.toTimeString", + "status": "static" + }, { "id": "stdlib.date.toUTCString", "kind": "stdlib", @@ -4830,8 +4862,8 @@ "id": "stdlib.number.toPrecision", "kind": "stdlib", "name": "number.prototype.toPrecision", - "status": "unsupported", - "code": "SC2012" + "status": "static", + "note": "the lowered call form takes 0 to 1 arguments" }, { "id": "stdlib.number.toString", @@ -5644,6 +5676,20 @@ "code": "SC2020", "note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)" }, + { + "id": "stdlib.webcrypto.CryptoKey", + "kind": "stdlib", + "name": "CryptoKey", + "status": "static", + "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + }, + { + "id": "stdlib.webcrypto.subtle", + "kind": "stdlib", + "name": "globalThis.crypto.subtle", + "status": "static", + "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + }, { "id": "syntax.async-generators", "kind": "syntax", diff --git a/packages/compiler/test/match-row-emission.test.ts b/packages/compiler/test/match-row-emission.test.ts new file mode 100644 index 000000000..39e07bc1c --- /dev/null +++ b/packages/compiler/test/match-row-emission.test.ts @@ -0,0 +1,69 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { compile } from "../src/index.js"; + +const directories: string[] = []; +afterEach(async () => { + await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +async function emit(source: string): Promise { + const outDir = await mkdtemp(join(tmpdir(), "scriptc-match-rows-")); + directories.push(outDir); + const entry = join(outDir, "main.ts"); + await writeFile(entry, source); + const outPath = join(outDir, "program.ll"); + const result = await compile(entry, { entry, outDir, outPath, outputKind: "llvm" }); + expect(result.ok, JSON.stringify(result)).toBe(true); + return readFile(outPath, "utf8"); +} + +const calls = (llvm: string, symbol: string): number => + llvm.split("\n").filter((line) => line.includes("call ") && line.includes(`@${symbol}(`)).length; + +test("rows read only through their elements carry no match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.index, m[1], m.groups!.val, 1 in m); +console.log([...text.matchAll(re)].map((m) => m[2]).join(",")); +console.log([...text.matchAll(re)].filter((m) => m.length > 2).find((m) => m[1] === "k2")?.[2]); +console.log(Array.from(text.matchAll(re), ({ groups }) => groups!.key).join(",")); +const rows = [...text.matchAll(re)]; +for (const row of rows.slice(1)) console.log(row[0]); +`); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(4); + expect(calls(llvm, "scr_regex_match_all")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(0); +}); + +test("rows whose properties are read or that escape keep match metadata", async () => { + const llvm = await emit(` +const text = "k1=v1; k2=v2;"; +const re = /(?k\\d)=(?v\\d);/g; +for (const m of text.matchAll(re)) console.log(m.input.length); +console.log([...text.matchAll(re)].map((m) => m.index).join(",")); +function first(m: RegExpExecArray): string { + return m[1]!; +} +console.log([...text.matchAll(re)].map(first).join(",")); +console.log([...text.matchAll(re)].map((m) => m)[0]![1]); +`); + expect(calls(llvm, "scr_regex_match_all_into")).toBe(1); + expect(calls(llvm, "scr_regex_match_all")).toBe(3); + expect(calls(llvm, "scr_regex_match_all_captures")).toBe(0); + expect(calls(llvm, "scr_regex_match_all_captures_into")).toBe(0); +}); + +test(".index and .input on a match slice read its match source", async () => { + const llvm = await emit(` +const rows = [..."a1b2".matchAll(/[a-z](\\d)/g)]; +console.log(rows[1]!.index, rows[0]!.input); +const m = /b/.exec("ab")!; +console.log(m.index); +`); + expect(calls(llvm, "scr_regex_result_prop")).toBe(3); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json new file mode 100644 index 000000000..c95861fe3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/1112-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/1112-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json new file mode 100644 index 000000000..58a162c8f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5639-union-arm-by-discriminant.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5639-union-arm-by-discriminant.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json new file mode 100644 index 000000000..091c37141 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5640-generic-structural-class-narrowing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5640-generic-structural-class-narrowing.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/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/6023-string-compare-utf16.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json new file mode 100644 index 000000000..365709f61 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6023-string-compare-utf16.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6023-string-compare-utf16.ts" + ], + "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/6077-generator-helpers-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json new file mode 100644 index 000000000..3d3c69f19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6077-generator-helpers-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6077-generator-helpers-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json new file mode 100644 index 000000000..6feb69d8e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6078-destructure-untyped-iterables.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6078-destructure-untyped-iterables.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json new file mode 100644 index 000000000..3d94dc336 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6079-generator-helpers-any.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6079-generator-helpers-any.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json new file mode 100644 index 000000000..ff4454b22 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6080-take-then-flatmap.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6080-take-then-flatmap.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/6200-string-annexb-locale-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json new file mode 100644 index 000000000..17a5c1e30 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6200-string-annexb-locale-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6200-string-annexb-locale-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json new file mode 100644 index 000000000..b80384ada --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6201-number-precision-exponential.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6201-number-precision-exponential.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json new file mode 100644 index 000000000..dbf5146ed --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6202-dyn-string-number-methods.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6202-dyn-string-number-methods.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json new file mode 100644 index 000000000..42b17b7bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6203-any-string-number-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6203-any-string-number-methods.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json new file mode 100644 index 000000000..86fcbcf58 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6204-crypto-get-random-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6204-crypto-get-random-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json new file mode 100644 index 000000000..42d90fd62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6205-crypto-get-random-values.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6205-crypto-get-random-values.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json new file mode 100644 index 000000000..7a9c25279 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6206-buffer-is-buffer-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6206-buffer-is-buffer-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json new file mode 100644 index 000000000..6f24442b0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6207-fs-constants-object.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6207-fs-constants-object.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json new file mode 100644 index 000000000..eac8fa544 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6208-fs-constants-require.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6208-fs-constants-require.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json new file mode 100644 index 000000000..3d3c194b2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6209-buffer-module-alias.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6209-buffer-module-alias.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json new file mode 100644 index 000000000..33ccb2715 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6210-typeof-global-static-members.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6210-typeof-global-static-members.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json new file mode 100644 index 000000000..bcd9bcf0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6260-js-var-dyn-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6260-js-var-dyn-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json new file mode 100644 index 000000000..3733ef399 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6261-js-destructure-for-of-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6261-js-destructure-for-of-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json new file mode 100644 index 000000000..2db0c3083 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6262-js-file-scope-var-identity.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6262-js-file-scope-var-identity.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json new file mode 100644 index 000000000..d0e4d3fcb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6263-js-factory-return-identity.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6263-js-factory-return-identity.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json new file mode 100644 index 000000000..6b7a65b24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6291-webcrypto-hmac-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json new file mode 100644 index 000000000..a1b096f32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6292-webcrypto-hmac-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json new file mode 100644 index 000000000..7200349f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6293-webcrypto-digest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json new file mode 100644 index 000000000..c68b4b49c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6294-webcrypto-js-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json new file mode 100644 index 000000000..286b2c4bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6295-jose-hs256/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json new file mode 100644 index 000000000..05d9557e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6296-array-position-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json new file mode 100644 index 000000000..3c3818490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6297-library-runtime-idioms.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..ad7513645 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6331-jose-root-entry/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..d848dd493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6332-fluent-this-declaration/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..4bde02942 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6333-fluent-this-declaration-js/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..e510d9e2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6334-discarded-logical-statements.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..fd7b159a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6335-discarded-logical-statements.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json new file mode 100644 index 000000000..89aef0f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6340-prototype-reflection-optional-view-param.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json new file mode 100644 index 000000000..e4f556b43 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6350-globalthis-assertion-state.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6350-globalthis-assertion-state.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json new file mode 100644 index 000000000..2d11062c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6352-globalthis-state-modules/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6352-globalthis-state-modules/boot.ts", + "/tests/corpus/6352-globalthis-state-modules/state.ts", + "/tests/corpus/6352-globalthis-state-modules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json new file mode 100644 index 000000000..5975cbb40 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-late-optional.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-late-optional.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json new file mode 100644 index 000000000..7b99fa5f6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-literal-flows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-literal-flows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json new file mode 100644 index 000000000..43b336a1b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/key-order-spread-copies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/key-order-spread-copies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json deleted file mode 100644 index 27b24a99a..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-1112-number-methods.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-1112-number-methods.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/webcrypto-refusals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json new file mode 100644 index 000000000..5f9d95858 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/webcrypto-refusals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json new file mode 100644 index 000000000..27fe582a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/navigator.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json new file mode 100644 index 000000000..af889405d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-hmac.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/parity.test.ts b/packages/compiler/test/ts7/parity.test.ts index 872676c81..4904f2720 100644 --- a/packages/compiler/test/ts7/parity.test.ts +++ b/packages/compiler/test/ts7/parity.test.ts @@ -488,7 +488,7 @@ test("symbol declarations resolve back into the same client AST", () => { expect(sym7).toBeDefined(); const aliased = w.c7.getAliasedSymbol(sym7!); expect(aliased.name).toBe("answer"); - const declHandle = aliased.declarations[0]; + const declHandle = aliased.declarationHandles[0]; expect(declHandle).toBeDefined(); const decl = declHandle!.resolve(w.p7.project); expect(decl).toBeDefined(); 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..28b63ec34 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, test } from "vitest"; -import { API, type Snapshot } from "typescript/unstable/sync"; +import { API, type Snapshot, type Symbol as SdkSymbol } from "typescript/unstable/sync"; import { Ts7Api, ts7Executable } from "../../src/frontend/ts7/rpc-api.js"; import { Ts7RpcClient } from "../../src/frontend/ts7/rpc-client.js"; import type { Ts7FileSystem } from "../../src/frontend/ts7/rpc-filesystem.js"; @@ -16,6 +16,15 @@ import { SemanticType, } from "../../src/frontend/ts7/semantic-model.js"; +// scriptc's symbols keep their declarations' node handles in +// `declarationHandles`, and the SDK's in `declarations`. +function declarationHandlesOf(symbol: SdkSymbol): SdkSymbol["declarations"] { + return ( + (symbol as unknown as { declarationHandles?: SdkSymbol["declarations"] }).declarationHandles ?? + symbol.declarations + ); +} + function tsgoPath(path: string): string { return process.platform === "win32" ? path.replaceAll("\\", "/") : path; } @@ -43,7 +52,7 @@ function programFacts(snapshot: Snapshot, config: string, path: string) { text: file.text, type: checker.typeToString(type), symbol: symbol.name, - declaration: symbol.declarations[0]!.resolve()!.getText(file), + declaration: declarationHandlesOf(symbol)[0]!.resolve()!.getText(file), syntactic: project.program.getSyntacticDiagnostics(path), semantic: project.program.getSemanticDiagnostics(path), globals: project.program.getGlobalDiagnostics(), @@ -83,28 +92,32 @@ test("production RPC bridge preserves pinned SDK parser/checker behavior", () => expect(project.checker.getTypeAtPosition(path, source.text.indexOf("answer"))).toBe(first); const symbol = project.checker.getSymbolAtPosition(path, source.text.indexOf("answer")); expect(symbol).toBeInstanceOf(SemanticSymbol); - expect(symbol!.declarations[0]).toBeInstanceOf(SemanticNodeHandle); + expect(symbol!.declarationHandles[0]).toBeInstanceOf(SemanticNodeHandle); expect(project.checker.getSymbolAtPosition(path, source.text.indexOf("answer"))).toBe(symbol); - expect(symbol!.declarations[0]!.resolve()).toBe(symbol!.declarations[0]!.resolve()); - const declaration = symbol!.declarations[0]!.resolve()!; + expect(symbol!.declarationHandles[0]!.resolve()).toBe(symbol!.declarationHandles[0]!.resolve()); + const declaration = symbol!.declarationHandles[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 +193,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,18 +207,22 @@ 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, sharedType: typeA === typeB, sharedSignature: signatureA === signatureB, - sameDeclaration: symbolA.declarations[0]!.resolve() === symbolB.declarations[0]!.resolve(), + sameDeclaration: + declarationHandlesOf(symbolA)[0]!.resolve() === + declarationHandlesOf(symbolB)[0]!.resolve(), explicitDeclaration: - symbolB.declarations[0]!.resolve(b) === - (fileB.statements[1] as import("typescript/unstable/ast").VariableStatement) - .declarationList.declarations[0], + declarationHandlesOf(symbolB)[0]!.resolve(b) === + 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), }, @@ -227,7 +248,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", expect(next.signatureA).not.toBe(actual.signatureA); first.dispose(); expect(() => actual.typeA.getSymbol()).toThrow("disposed"); - expect(() => actual.symbolA.declarations[0]!.resolve()).toThrow("disposed"); + expect(() => declarationHandlesOf(actual.symbolA)[0]!.resolve()).toThrow("disposed"); expect(next.signatureA.getParameters()[0]!.name).toBe("value"); expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42"); second.dispose(); diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index d4607f694..11367335c 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -113,6 +113,7 @@ const optional = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -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 96add68b6..dacd28937 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -103,6 +103,7 @@ const OPTIONAL = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; @@ -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_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 ffcb85050..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) { diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index d33f32a97..64fc36484 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -293,6 +293,9 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) { return scr_bytes_retain(bytes); } +/* Nothing on an existing path calls this. */ +bool scr_bytes_is_buffer(const ScrBytes *bytes) { return bytes->is_buffer; } + void scr_bytes_release(ScrBytes *b) { if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */ if (--b->rc == 0) { diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 993921043..b7b913d7f 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -159,6 +159,8 @@ ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */ /* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */ ScrBytes *scr_bytes_raw_view(ScrBytes *bytes); ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */ +/* Buffer.isBuffer over a typed view: the view's Buffer brand. Borrows. */ +bool scr_bytes_is_buffer(const ScrBytes *bytes); /* Views retain the root allocation; offsets are relative to its complete * byte storage, including when src is itself a subarray. */ ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem, diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 70fa94dd2..4b6b6a6a6 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -1,4 +1,5 @@ -/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by +/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c) and the + * WebCrypto HMAC slice (scr_webcrypto.c). Included by * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_CRYPTO_H #define SCR_CRYPTO_H @@ -50,6 +51,10 @@ ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset); double scr_crypto_random_int(double min, double max); double scr_crypto_random_int_max(double max); ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size); +/* crypto.getRandomValues over a view (returned +1) or any value (the + * value itself, +1); NULL with Node's DOMException pending. */ +ScrBytes *scr_crypto_get_random_values(ScrBytes *view); +ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value); double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size, int form); void scr_crypto_random_int_refill(void); @@ -189,6 +194,22 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t unsigned char out[64]); size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); +/* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── + * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and + * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * call's argument vector as one dyn array (borrowed) and answers a +1 + * promise that every argument or validation failure rejects with Node's + * TypeError or DOMException; algorithms and formats outside the slice + * reject with a coded SC2020 Error. importKey settles at once with a + * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and + * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key + * answers `value instanceof CryptoKey`. */ +ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_sign(ScrDyn *args); +ScrPromise *scr_webcrypto_verify(ScrDyn *args); +ScrPromise *scr_webcrypto_digest(ScrDyn *args); +bool scr_webcrypto_is_crypto_key(const ScrDyn *value); + /* The composed `new crypto.X509Certificate(data).fingerprint` read (the * handle never materializes): the SHA-1 of the DER, uppercase * colon-separated — PEM or raw-DER input; anything else throws Node's 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_dyn.h b/packages/runtime/src/scr_dyn.h index 2606e1473..68b2af8d6 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -137,6 +137,7 @@ typedef enum { 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) */ @@ -162,6 +163,7 @@ typedef enum { 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) */ @@ -702,6 +704,32 @@ 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); +/* A compiled program that boxes generators and calls iterator helpers on + * untyped values registers the checked functions that read a generator + * capsule's next and return methods (each takes the capsule). The + * iterator helpers below then drive compiled generators. */ +void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter); +/* An iterator helper method called on a synchronous generator capsule: + * true with *out set (+1, or NULL with the exception pending) when + * `method` is one. NULL until the program registers its generator + * members. */ +extern SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *recv, const char *method, + ScrDyn *const *args, size_t argc, + ScrDyn **out); +/* The Iterator.prototype helper method `name` as a function value (+1), + * the value a compiled generator's helper property reads; undefined for + * other names. */ +ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name); /* The ordinary object snapshot plus a retained typed source. source_access * releases that source when materialize=false and returns a fresh snapshot * when true. */ @@ -846,6 +874,19 @@ void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what); * compile-time source spelling), else the runtime kind wording. Borrows * both; NULL with the exception pending on the throw. */ ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg); +/* A rest element's pack (`const [a, ...rest] = d`): scr_dyn_iter_pack's + * kinds pack as there, and other iterables step through the iterator + * protocol to the end. +1, or NULL with the error pending. */ +ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg); +/* Destructuring's first `n` values (`const [a, b] = d`) of a dyn source + * other than an array, string or bytes (the emitted walk packs those), + * through the iterator protocol: an iterator with values left closes. + * `spell` is the compile-time not-iterable wording, or NULL. +1, or NULL + * with the error pending. */ +ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell); +/* True for typed capsules that iterate through the protocol (a compiled + * synchronous generator) rather than through their materialized view. */ +bool scr_dyn_iterates_by_protocol(const ScrDyn *value); /* Map constructor seeds: retain array inputs for live iteration, accept * nullish inputs as empty, and validate each entry before reading 0/1. */ ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src); @@ -881,8 +922,15 @@ 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); +/* This thread's global object box (borrowed). Every spelling of the global + * object answers it; its symbol-keyed properties are the global object's + * (scr_node_builtin.c). */ +ScrDyn *scr_global_object(void); ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */ void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */ bool scr_dyn_global_symbol_has(ScrSym *key); diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index a3085399f..8a1bfa728 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -559,10 +559,8 @@ static ScrDyn *scr_dynh_dispatch(ScrDyn *recv, const char *method, ScrDyn *const * reaching the comparator. The ordered list writes back index by index, so * a comparator that mutates the receiver mid-sort never dangles the items * being ordered. The default comparator compares ToString images - * (scr_dyn_display_buf — join's conversion) bytewise: code-POINT order, - * where JS orders UTF-16 code units — identical through the BMP, divergent - * only across the surrogate boundary (SEMANTICS.md). A comparator result - * converts with ToNumber; NaN counts as 0. */ + * (scr_dyn_display_buf — join's conversion) in UTF-16 code-unit order. A + * comparator result converts with ToNumber; NaN counts as 0. */ static ScrStr *dyn_sort_str(const ScrDyn *e) { ScrJsonBuf b; scr_jb_init(&b); @@ -584,7 +582,7 @@ static bool dyn_sort_less(void *ctx, uint64_t xs, uint64_t ys, bool *failed) { } ScrStr *xstr = dyn_sort_str(x); ScrStr *ystr = dyn_sort_str(y); - int c = scr_str_cmp(xstr, ystr); + int c = scr_str_cmp_u16(xstr, ystr); scr_str_release(xstr); scr_str_release(ystr); return c < 0; @@ -815,6 +813,10 @@ static ScrDyn *scr_dyn_invoke_impl( bool handled = false; ScrDyn *fast = dyn_live_array_method(recv, method, args, argc, what, &handled); if (handled) return fast; + /* Iterator helpers on a compiled generator. */ + if (scr_dyn_generator_helper_hook && + scr_dyn_generator_helper_hook(recv, method, args, argc, &fast)) + return fast; if (scr_event_ref_hooks && scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast)) return fast; diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h index a92cd3222..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); 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 4aa703711..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); } @@ -1058,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"; @@ -1309,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 @@ -1349,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) { @@ -1366,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. */ @@ -1377,7 +1503,11 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { if (!cls) scr_trap("scriptc: out of memory\n"); memcpy(cls, name->data, name->len); cls[name->len] = '\0'; + size_t nseen = g_nseen; + g_pending_seen = instance; out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; free(cls); } else { out = scr_insp_dyn(materialized, recurse, depth); diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h index 4414330b5..4eb607a5c 100644 --- a/packages/runtime/src/scr_inspect.h +++ b/packages/runtime/src/scr_inspect.h @@ -55,8 +55,8 @@ void scr_insp_entry(ScrStr *s, bool is_num); * FIRST (a value already on the traversal stack answers its circular id, * assigned in discovery order — the caller renders scr_insp_circular and * descends no further), seen_push after begin, and ref_wrap around end's - * result (pops the stack; values in the circular map gain the " " - * prefix). State resets at each top-level value's first frame + * result (pops the stack; scr_insp_end gives values in the circular map + * the " " prefix). State resets at each top-level value's first frame * (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */ double scr_insp_circ_check(const void *v); void scr_insp_seen_push(const void *v); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 0376ece2c..6174913c0 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; @@ -4673,10 +4675,48 @@ 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); +static bool scr_dyn_sync_generator(const ScrDyn *value); +extern SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t); + +/* scr_iterator_read of an iterator's own next or return method: a + * compiled generator answers through the program's registered getters. */ +static ScrDyn *scr_iterator_member(const ScrDyn *iterator, const char *key, size_t len) { + if (scr_generator_member_hook && scr_dyn_sync_generator(iterator)) { + ScrDyn *member = scr_generator_member_hook(iterator, key, len); + if (member) return member; + } + return scr_iterator_read(iterator, key, len); +} + static ScrDyn *scr_native_iterator_get(void *ptr, const char *key, size_t len) { if (len != 4 || memcmp(key, "next", 4)) { /* The Iterator.prototype helper methods. */ @@ -4866,7 +4906,7 @@ static bool scr_ih_budget(const ScrDyn *limit, double *out) { * Node's TypeError. Answers false when an exception is pending after. */ static bool scr_ih_close(ScrDyn *iterator, bool abrupt) { ScrCaught *first = abrupt ? scr_exc_take() : NULL; - ScrDyn *method = scr_iterator_read(iterator, "return", 6); + ScrDyn *method = scr_iterator_member(iterator, "return", 6); bool ok = method != NULL; if (ok && method->kind != SCR_DYN_UNDEF && method->kind != SCR_DYN_NULL) { ScrDyn *result = scr_iterator_call(method, iterator, "return"); @@ -4933,7 +4973,7 @@ static ScrDyn *scr_ih_direct(const ScrDyn *self, const char *method) { scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return NULL; } - return scr_iterator_read(self, "next", 4); + return scr_iterator_member(self, "next", 4); } static ScrDyn *scr_ih_result(ScrDyn *value, bool done) { @@ -4990,7 +5030,7 @@ static ScrDyn *scr_ih_flattenable(ScrDyn *value, ScrDyn **next_out) { } scr_dyn_release(method); if (!iterator) return NULL; - ScrDyn *next = scr_iterator_read(iterator, "next", 4); + ScrDyn *next = scr_iterator_member(iterator, "next", 4); if (!next) { scr_dyn_release(iterator); return NULL; @@ -6202,6 +6242,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); @@ -6234,6 +6288,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); @@ -6914,6 +6975,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, @@ -6952,7 +7022,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); @@ -7376,6 +7446,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. */ @@ -7396,8 +7474,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); @@ -7406,7 +7490,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) { @@ -7539,7 +7623,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; @@ -7563,9 +7647,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) { @@ -7863,6 +7947,14 @@ static void scr_dyn_object_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value, con return; } +void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn *value) { + if (!arr->v.arr.properties) { + scr_dyn_region_store(arr, NULL); /* a table that later holds anything */ + arr->v.arr.properties = scr_dyn_new_obj(); + } + scr_dyn_obj_set(arr->v.arr.properties, key, key_len, value); +} + void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { if (scr_dyn_class_reflection_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } @@ -13888,3 +13980,115 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) { return scr_dyn_new_obj(); } } + +/* ── compiled generators through untyped values ─────────────────────── */ + +static SCR_TL ScrDyn *scr_generator_next_getter; +static SCR_TL ScrDyn *scr_generator_return_getter; +/* Set with the getters, so only programs that register them link the + * iterator helpers through these paths. External, so the compiler keeps + * the calls through them indirect. */ +SCR_TL ScrDyn *(*scr_generator_member_hook)(const ScrDyn *, const char *, size_t) = NULL; +SCR_TL bool (*scr_dyn_generator_helper_hook)(ScrDyn *, const char *, ScrDyn *const *, size_t, + ScrDyn **) = NULL; +static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len); +static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, + size_t argc, ScrDyn **out); + +static void scr_generator_members_cleanup(void) { + scr_dyn_release(scr_generator_next_getter); + scr_dyn_release(scr_generator_return_getter); + scr_generator_next_getter = NULL; + scr_generator_return_getter = NULL; + scr_generator_member_hook = NULL; + scr_dyn_generator_helper_hook = NULL; +} + +void scr_dyn_set_generator_members(ScrDyn *next_getter, ScrDyn *return_getter) { + if (!scr_generator_next_getter) scr_atexit(scr_generator_members_cleanup); + scr_dyn_release(scr_generator_next_getter); + scr_dyn_release(scr_generator_return_getter); + scr_generator_next_getter = scr_dyn_retain(next_getter); + scr_generator_return_getter = scr_dyn_retain(return_getter); + scr_generator_member_hook = &scr_generator_member; + scr_dyn_generator_helper_hook = &scr_generator_helper_invoke; +} + +static bool scr_dyn_sync_generator(const ScrDyn *value) { + return scr_dyn_generator(value) && value->v.typed_ref.type_key[0] == 'g'; +} + +/* A synchronous generator capsule's next or return method (+1), read + * through the program's registered getters. NULL for other keys. */ +static ScrDyn *scr_generator_member(const ScrDyn *capsule, const char *key, size_t len) { + ScrDyn *getter = len == 4 && !memcmp(key, "next", 4) ? scr_generator_next_getter + : len == 6 && !memcmp(key, "return", 6) ? scr_generator_return_getter : NULL; + if (!getter) return NULL; + ScrDyn *args[1] = { (ScrDyn *)capsule }; + return scr_dyn_call(getter, args, 1, "next"); +} + +static bool scr_generator_helper_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, + size_t argc, ScrDyn **out) { + if (!scr_dyn_sync_generator(recv)) return false; + int index = scr_ih_method_index(method, strlen(method)); + if (index < 0) return false; + *out = scr_iterator_helper_method(recv, index, args, argc); + return true; +} + +ScrDyn *scr_dyn_iterator_helper_method(const ScrStr *name) { + int index = scr_ih_method_index(name->data, name->len); + return index >= 0 ? scr_ih_method_function(index) : scr_dyn_retain(scr_dyn_undefined()); +} + +/* Destructuring over an iterable the pack walk cannot list (a compiled + * generator, an iterator or iterator helper, a Map or Set): GetIterator, + * then the first `n` values through the iterator protocol, or every value + * when `all`, into a fresh array. Positions past the end read undefined, + * and an iterator with values left closes. `spell` is the compile-time + * not-iterable wording, or NULL. +1, or NULL with the error pending. */ +static ScrDyn *scr_dyn_iter_steps(const ScrDyn *src, const ScrStr *spell, size_t n, bool all) { + ScrDyn *iterator = scr_dyn_iterator(src, spell); + if (!iterator) return NULL; + ScrDyn *next = scr_iterator_member(iterator, "next", 4); + if (!next) { + scr_dyn_release(iterator); + return NULL; + } + ScrDyn *out = scr_dyn_new_arr(); + bool done = false; + for (size_t i = 0; all ? !done : i < n; i++) { + ScrDyn *value = done ? scr_dyn_retain(scr_dyn_undefined()) : scr_ih_step(iterator, next, &done); + if (!value) goto fail; + if (all && done) scr_dyn_release(value); + else scr_dyn_arr_push(out, value); + } + if (!done && !scr_ih_close(iterator, false)) goto fail; + scr_dyn_release(next); + scr_dyn_release(iterator); + return out; +fail: + scr_dyn_release(out); + scr_dyn_release(next); + scr_dyn_release(iterator); + return NULL; +} + +ScrDyn *scr_dyn_destructure_rest(const ScrDyn *src, const ScrStr *msg) { + bool packs = src->kind == SCR_DYN_ARR || src->kind == SCR_DYN_STR || src->kind == SCR_DYN_BYTES || + (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack) || + (src->kind == SCR_DYN_TYPED_REF && !scr_dyn_iterates_by_protocol(src)); + return packs ? scr_dyn_iter_pack(src, msg) : scr_dyn_iter_steps(src, msg, 0, true); +} + +ScrDyn *scr_dyn_iter_take(const ScrDyn *src, size_t n, const char *spell) { + ScrStr *spelling = spell && *spell ? scr_str_new(spell, strlen(spell)) : NULL; + ScrDyn *out = scr_dyn_iter_steps(src, spelling, n, false); + scr_str_release(spelling); + return out; +} + +bool scr_dyn_iterates_by_protocol(const ScrDyn *value) { + return scr_dyn_sync_generator(value); +} diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index c0b285dc1..930d44818 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -10746,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; @@ -10782,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) { @@ -10865,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; } @@ -10904,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 @@ -11045,6 +11408,137 @@ ScrStr *scr_num_to_fixed(double x, double fraction_digits) { return scr_str_new(out, len); } +/* String.prototype.substr (Annex B.2.2.1) over substring's UTF-16 + * clamp: -Infinity and other starts below -size clamp to 0, an Infinity + * length to the end. Comparisons only (no floating-point literal other + * than zero, which would join the constant pool every binary links). + * Nothing on an existing path calls this. */ +ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length) { + double size = scr_str_utf16_len(s); + double first = isnan(start) ? 0 : trunc(start); + if (first < 0) { + first = size + first; + if (first < 0) first = 0; + } else if (first > size) { + first = size; + } + double count = has_length ? (isnan(length) ? 0 : trunc(length)) : size; + if (count < 0) count = 0; + double end = first + count; + if (end > size) end = size; + return scr_str_substring(s, first, end); +} + +/* The exponent suffix of toPrecision/toExponential: "e+5", "e-7". */ +static size_t scr_num_exp_suffix(char *o, int e) { + size_t n = 0; + o[n++] = 'e'; + o[n++] = e < 0 ? '-' : '+'; + int a = e < 0 ? -e : e; + char rev[8]; + int k = 0; + do { + rev[k++] = (char)('0' + a % 10); + a /= 10; + } while (a != 0); + while (k > 0) o[n++] = rev[--k]; + return n; +} + +/* Number.prototype.toPrecision(precision) (ECMA-262 §21.1.3.5): undefined + * precision is ToString(x); ToIntegerOrInfinity runs (in the caller's + * coercion) before the non-finite arm, which precedes the 1..100 range + * check. Digits are the exact-value, ties-up n of + * scr_f64_precision_digits; exponential form when e < -6 or e >= p. + * Nothing on an existing path calls this. */ +ScrStr *scr_num_to_precision(double x, double precision, bool has_precision) { + if (!has_precision || !isfinite(x)) return scr_f64_to_scrstr(x); + double p = isnan(precision) ? 0 : trunc(precision); + if (!(p >= 1 && p <= 100)) { + static const char msg[] = "toPrecision() argument must be between 1 and 100"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + int digits_count = (int)p; + char out[160]; + size_t o = 0; + double a = x; + if (x < 0) { + out[o++] = '-'; + a = -x; + } + char d[104]; + int e = 0; + if (a == 0) { + memset(d, '0', (size_t)digits_count); + d[digits_count] = '\0'; + } else { + scr_f64_precision_digits(a, digits_count, d, &e); + } + if (e < -6 || e >= digits_count) { + out[o++] = d[0]; + if (digits_count != 1) { + out[o++] = '.'; + memcpy(out + o, d + 1, (size_t)(digits_count - 1)); + o += (size_t)(digits_count - 1); + } + o += scr_num_exp_suffix(out + o, e); + } else if (e >= 0) { + memcpy(out + o, d, (size_t)(e + 1)); + o += (size_t)(e + 1); + if (e + 1 < digits_count) { + out[o++] = '.'; + memcpy(out + o, d + e + 1, (size_t)(digits_count - e - 1)); + o += (size_t)(digits_count - e - 1); + } + } else { + out[o++] = '0'; + out[o++] = '.'; + for (int i = 0; i < -(e + 1); i++) out[o++] = '0'; + memcpy(out + o, d, (size_t)digits_count); + o += (size_t)digits_count; + } + return scr_str_new(out, o); +} + +/* Number.prototype.toExponential(fractionDigits) with a DEFINED argument + * (the undefined form is scr_num_to_exponential's shortest mantissa): + * f + 1 exact-value, ties-up digits, "d.ddde±X". Nothing on an existing + * path calls this. */ +ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits) { + if (!isfinite(x)) return scr_f64_to_scrstr(x); + double f = isnan(fraction_digits) ? 0 : trunc(fraction_digits); + if (!(f >= 0 && f <= 100)) { + static const char msg[] = "toExponential() argument must be between 0 and 100"; + scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); + return NULL; + } + int count = (int)f + 1; + char out[160]; + size_t o = 0; + double a = x; + if (x < 0) { + out[o++] = '-'; + a = -x; + } + char d[104]; + int e = 0; + if (a == 0) { + memset(d, '0', (size_t)count); + d[count] = '\0'; + } else { + scr_f64_precision_digits(a, count, d, &e); + } + out[o++] = d[0]; + if (count != 1) { + out[o++] = '.'; + memcpy(out + o, d + 1, (size_t)(count - 1)); + o += (size_t)(count - 1); + } + o += scr_num_exp_suffix(out + o, e); + return scr_str_new(out, o); +} + /* Increment a decimal digit string in place. Returns true on overflow — * the value becomes 1 followed by len zeros (the caller folds the zeros * into its scale); an EMPTY string increments to "1" the same way (the @@ -11240,11 +11734,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); @@ -11292,7 +11800,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", @@ -11360,3 +11870,52 @@ ScrDyn *scr_dyn_native_date_from_ms(double ms) { scr_native_date_release(date); return result; } + +/* crypto.getRandomValues(typedArray) (WebCrypto, also node:crypto's + * export): integer typed arrays only (Node's TypeMismatchError DOMException + * for float arrays, DataViews and every other value), at most 65536 bytes + * (QuotaExceededError), filled in place and returned. Nothing on an + * existing path calls these. They stay at the end of the unit: clang emits + * a static helper (scr_random_write) where its first emitted caller + * appears, and an earlier caller would move it in every binary that links + * the random fills. */ +ScrBytes *scr_crypto_get_random_values(ScrBytes *view) { + static const char mismatch[] = "TypeMismatchError"; + static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray"; + static const char quota[] = "QuotaExceededError"; + static const char quota_msg[] = "The requested length exceeds 65,536 bytes"; + if (view->is_data_view || view->elem == SCR_BYTES_F32 || view->elem == SCR_BYTES_F64 || + view->elem == SCR_BYTES_F16) { + scr_throw_domex(mismatch, mismatch_msg); + return NULL; + } + /* Integer arithmetic: a new floating-point literal would land in the + * merged constant pool every binary links. */ + size_t length = view->len * (size_t)scr_bytes_elem_size(view->elem); + if (length > 65536u) { + scr_throw_domex(quota, quota_msg); + return NULL; + } + scr_random_write(view, 0, length); + return scr_bytes_retain(view); +} + +ScrDyn *scr_crypto_get_random_values_dyn(ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (!materialized) return NULL; + ScrDyn *result = scr_crypto_get_random_values_dyn(materialized); + scr_dyn_release(materialized); + return result ? scr_dyn_retain(value) : NULL; + } + if (value->kind != SCR_DYN_BYTES) { + static const char mismatch[] = "TypeMismatchError"; + static const char mismatch_msg[] = "The data argument must be an integer-type TypedArray"; + scr_throw_domex(mismatch, mismatch_msg); + return NULL; + } + ScrBytes *filled = scr_crypto_get_random_values(value->v.bytes); + if (!filled) return NULL; + scr_bytes_release(filled); + return scr_dyn_retain(value); +} diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c index 34c700350..233617092 100644 --- a/packages/runtime/src/scr_node_builtin.c +++ b/packages/runtime/src/scr_node_builtin.c @@ -86,6 +86,10 @@ ScrDyn *scr_util_system_error_entries(void) { static SCR_TL ScrDyn *scr_global_known; static SCR_TL ScrDyn *scr_global_own; +/* The one box denoting this thread's global object. Symbol-keyed properties + * live on the box (string-keyed ones in scr_global_own), so every spelling + * of the global object answers this box. */ +static SCR_TL ScrDyn *scr_global_box; static SCR_TL bool scr_global_cleanup_registered; static SCR_TL ScrDyn *(*scr_global_console_get)(void); static SCR_TL ScrDyn *(*scr_global_fetch_get)(void); @@ -180,6 +184,8 @@ ScrDyn *scr_process_hrtime_value(void) { } static void scr_global_cleanup(void) { + scr_dyn_release(scr_global_box); + scr_global_box = NULL; scr_dyn_release(scr_global_known); scr_dyn_release(scr_global_own); scr_global_known = NULL; @@ -195,7 +201,7 @@ static ScrDyn *scr_global_get(void *handle, const char *key, size_t length) { if (own) return scr_dyn_retain(own); if ((length == 10 && memcmp(key, "globalThis", length) == 0) || (length == 6 && memcmp(key, "global", length) == 0)) - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(scr_global_box); if (length == 7 && memcmp(key, "console", length) == 0 && scr_global_console_get) return scr_global_console_get(); if (length == 5 && memcmp(key, "fetch", length) == 0 && scr_global_fetch_get) return scr_global_fetch_get(); @@ -223,26 +229,32 @@ static bool scr_global_set(void *handle, const char *key, size_t length, const S return true; } -ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { +ScrDyn *scr_global_object(void) { static const ScrDynHandleOps ops = { .cls = "Object", .retain = scr_global_retain, .release = scr_global_release, .get = scr_global_get, .set = scr_global_set, }; scr_dyn_handle_install(SCR_DYNH_GLOBAL, &ops); - scr_global_console_get = console_get; if (!scr_global_known) scr_global_known = scr_dyn_new_obj(); if (!scr_global_own) scr_global_own = scr_dyn_new_obj(); + if (!scr_global_box) scr_global_box = scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); if (!scr_global_cleanup_registered) { scr_atexit(scr_global_cleanup); scr_global_cleanup_registered = true; } + return scr_global_box; +} + +ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) { + ScrDyn *global = scr_global_object(); + scr_global_console_get = console_get; for (size_t i = 0; i < known->len; i++) { ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i); if (!scr_dyn_obj_get(scr_global_known, key->data, key->len)) scr_dyn_obj_set(scr_global_known, key->data, key->len, scr_dyn_new_bool(true)); scr_str_release(key); } - return scr_dyn_new_handle(&scr_global_known, SCR_DYNH_GLOBAL); + return scr_dyn_retain(global); } typedef struct ScrBuiltinModule { diff --git a/packages/runtime/src/scr_number.c b/packages/runtime/src/scr_number.c index 1ccc414b7..dc8743e14 100644 --- a/packages/runtime/src/scr_number.c +++ b/packages/runtime/src/scr_number.c @@ -654,3 +654,163 @@ size_t scr_f64_to_fixed(double x, int f, char *buf) { *o = '\0'; return (size_t)(o - buf); } + +/* ── toPrecision / toExponential(fractionDigits) digit engine ────────── + * The spec's n and e for p significant digits: the integer n with + * 10^(p-1) <= n < 10^p for which n × 10^(e-p+1) - x is closest to zero, + * ties picking the LARGER n (half-up on the exact binary value). Exact + * big-integer arithmetic over the double's mantissa: the value scaled by + * 10^(p-1-e) is mantissa × 2^(binary_exp + f) × 5^f, and + * round_half_up(v) = floor((floor(2v) + 1) / 2) composes from floor + * divisions alone. Values reach ~1100 bits (5e-324 at 100 digits and + * 1.8e308), so the limbs are wider than toFixed's. Nothing on an existing + * path calls this. */ +#define SCR_PREC_LIMBS 40 +typedef struct { + uint32_t limb[SCR_PREC_LIMBS]; /* little-endian */ + int len; +} ScrPrecInt; + +static void scr_prec_normalize(ScrPrecInt *v) { + while (v->len > 1 && v->limb[v->len - 1] == 0) v->len--; +} + +static void scr_prec_mul_small(ScrPrecInt *v, uint32_t m) { + uint64_t carry = 0; + for (int i = 0; i < v->len; i++) { + uint64_t p = (uint64_t)v->limb[i] * m + carry; + v->limb[i] = (uint32_t)p; + carry = p >> 32; + } + if (carry != 0) v->limb[v->len++] = (uint32_t)carry; +} + +static uint32_t scr_prec_div_small(ScrPrecInt *v, uint32_t d) { + uint64_t rem = 0; + for (int i = v->len - 1; i >= 0; i--) { + uint64_t cur = (rem << 32) | v->limb[i]; + v->limb[i] = (uint32_t)(cur / d); + rem = cur % d; + } + scr_prec_normalize(v); + return (uint32_t)rem; +} + +static void scr_prec_shl(ScrPrecInt *v, int bits) { + uint32_t out[SCR_PREC_LIMBS] = {0}; + int words = bits / 32; + int rem = bits % 32; + for (int i = 0; i < v->len; i++) { + int at = i + words; + out[at] |= v->limb[i] << rem; + if (rem != 0 && at + 1 < SCR_PREC_LIMBS) out[at + 1] |= v->limb[i] >> (32 - rem); + } + int n = v->len + words + 1; + if (n > SCR_PREC_LIMBS) n = SCR_PREC_LIMBS; + memcpy(v->limb, out, sizeof out); + v->len = n; + scr_prec_normalize(v); +} + +static void scr_prec_shr(ScrPrecInt *v, int bits) { + int words = bits / 32; + int rem = bits % 32; + if (words >= v->len) { + v->limb[0] = 0; + v->len = 1; + return; + } + int n = v->len - words; + for (int i = 0; i < n; i++) { + uint32_t lo = v->limb[i + words] >> rem; + uint32_t hi = rem != 0 && i + words + 1 < v->len ? v->limb[i + words + 1] << (32 - rem) : 0; + v->limb[i] = lo | hi; + } + v->len = n; + scr_prec_normalize(v); +} + +/* round_half_up(a × 10^f) for positive finite a, as a decimal digit + * string written right-aligned ending at `end`; returns the first digit. */ +static char *scr_prec_scaled_digits(double a, int f, char *end) { + uint64_t bits; + memcpy(&bits, &a, sizeof bits); + uint64_t mantissa = bits & ((UINT64_C(1) << 52) - 1); + int ieee_exp = (int)((bits >> 52) & 0x7ffu); + int binary_exp; + if (ieee_exp == 0) { + binary_exp = -1074; + } else { + mantissa |= UINT64_C(1) << 52; + binary_exp = ieee_exp - 1023 - 52; + } + ScrPrecInt v = {{(uint32_t)mantissa, (uint32_t)(mantissa >> 32)}, 2}; + scr_prec_normalize(&v); + for (int i = 0; i < f; i++) scr_prec_mul_small(&v, 5); + int twos = binary_exp + f + 1; /* the +1 doubles: floor(2v) */ + if (twos > 0) scr_prec_shl(&v, twos); + for (int i = 0; i < -f; i++) scr_prec_div_small(&v, 5); + if (twos < 0) scr_prec_shr(&v, -twos); + /* (floor(2v) + 1) / 2 */ + uint64_t carry = 1; + for (int i = 0; i < v.len && carry != 0; i++) { + uint64_t s = (uint64_t)v.limb[i] + carry; + v.limb[i] = (uint32_t)s; + carry = s >> 32; + } + if (carry != 0) v.limb[v.len++] = (uint32_t)carry; + scr_prec_shr(&v, 1); + char *p = end; + for (;;) { + uint32_t chunk = scr_prec_div_small(&v, 1000000000u); + bool last = v.len == 1 && v.limb[0] == 0; + for (int i = 0; i < 9 && (chunk != 0 || !last || p == end); i++) { + *--p = (char)('0' + chunk % 10); + chunk /= 10; + } + if (last) break; + } + return p; +} + +int scr_f64_precision_digits(double a, int p, char *digits, int *e_out) { + char shortest[18]; + int n; + scr_f64_digits(a, shortest, &n); + int e = n - 1; + char buf[400]; + char *end = buf + sizeof buf; + for (int attempt = 0; attempt < 3; attempt++) { + char *first = scr_prec_scaled_digits(a, p - 1 - e, end); + int len = (int)(end - first); + if (len == p) { + memcpy(digits, first, (size_t)p); + digits[p] = '\0'; + *e_out = e; + return p; + } + if (len > p) { + /* Rounding carried into a new digit (or the estimate was low). */ + if (len == p + 1 && first[0] == '1') { + bool zeros = true; + for (int i = 1; i < len; i++) zeros = zeros && first[i] == '0'; + if (zeros) { + digits[0] = '1'; + memset(digits + 1, '0', (size_t)(p - 1)); + digits[p] = '\0'; + *e_out = e + 1; + return p; + } + } + e++; + } else { + e--; + } + } + /* Unreachable for finite positive doubles: the estimate is off by at + * most one decade. */ + memset(digits, '0', (size_t)p); + digits[p] = '\0'; + *e_out = e; + return p; +} diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h index 892625553..f43195116 100644 --- a/packages/runtime/src/scr_number.h +++ b/packages/runtime/src/scr_number.h @@ -37,6 +37,12 @@ int scr_decimal_scan(const char *p, size_t n, double *out); * the length. */ size_t scr_f64_to_fixed(double x, int f, char *buf); +/* toPrecision / toExponential(fractionDigits) digits for POSITIVE finite + * a and 1 <= p <= 101: the spec's p-digit n (ties to the larger n on the + * exact binary value) into digits (NUL-terminated, >= p + 1 bytes) and its + * decimal exponent e (value ~ d.ddd x 10^e) into *e_out. Returns p. */ +int scr_f64_precision_digits(double a, int p, char *digits, int *e_out); + /* ToString for template literals / string coercion. Returns +1. */ ScrStr *scr_f64_to_scrstr(double x); ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */ @@ -73,6 +79,11 @@ bool scr_num_is_safe_integer(double x); ScrStr *scr_num_to_exponential(double x); ScrStr *scr_num_to_fixed0(double x); ScrStr *scr_num_to_fixed(double x, double fraction_digits); +/* Number.prototype.toPrecision(precision) and toExponential(fractionDigits) + * with the argument already coerced (has_precision false = undefined). + * Invalid digit counts throw V8's RangeError (NULL). Result +1. */ +ScrStr *scr_num_to_precision(double x, double precision, bool has_precision); +ScrStr *scr_num_to_exponential_digits(double x, double fraction_digits); ScrStr *scr_num_to_string_radix_f64(double value, double radix); /* Object.is over two numbers — the spec's SameValue on doubles: NaN @@ -123,6 +134,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_os.c b/packages/runtime/src/scr_os.c index 2e70d0d85..500283995 100644 --- a/packages/runtime/src/scr_os.c +++ b/packages/runtime/src/scr_os.c @@ -1526,3 +1526,178 @@ ScrDyn *scr_node_constants(const ScrDyn *crypto_table) { scr_atexit(scr_node_constants_clear); return scr_dyn_retain(o); } + +/* fs.constants as a value: node_constants.cc's fs group (the set + * node:constants merges above), in its order and under its #ifdef guards, + * on its own cached null-prototype object with read-only members. It keeps + * its own table instead of calling the scr_fs_constants fill: a second + * caller would stop that static from inlining into scr_node_constants and + * change every binary that links node:constants. The names live inside + * the table entries, so the table is one data object section GC drops. + * Nothing on an existing path calls this. */ +typedef struct { + char name[32]; + double value; +} ScrFsConstant; +#define SCR_FS_C(n) {#n, (double)(n)} +static const ScrFsConstant scr_fs_constant_table[] = { + {"UV_FS_SYMLINK_DIR", 1}, + {"UV_FS_SYMLINK_JUNCTION", 2}, +#ifdef O_RDONLY + SCR_FS_C(O_RDONLY), +#endif +#ifdef O_WRONLY + SCR_FS_C(O_WRONLY), +#endif +#ifdef O_RDWR + SCR_FS_C(O_RDWR), +#endif + {"UV_DIRENT_UNKNOWN", 0}, + {"UV_DIRENT_FILE", 1}, + {"UV_DIRENT_DIR", 2}, + {"UV_DIRENT_LINK", 3}, + {"UV_DIRENT_FIFO", 4}, + {"UV_DIRENT_SOCKET", 5}, + {"UV_DIRENT_CHAR", 6}, + {"UV_DIRENT_BLOCK", 7}, +#ifdef S_IFMT + SCR_FS_C(S_IFMT), +#endif +#ifdef S_IFREG + SCR_FS_C(S_IFREG), +#endif +#ifdef S_IFDIR + SCR_FS_C(S_IFDIR), +#endif +#ifdef S_IFCHR + SCR_FS_C(S_IFCHR), +#endif +#ifdef S_IFBLK + SCR_FS_C(S_IFBLK), +#endif +#ifdef S_IFIFO + SCR_FS_C(S_IFIFO), +#endif +#ifdef S_IFLNK + SCR_FS_C(S_IFLNK), +#endif +#ifdef S_IFSOCK + SCR_FS_C(S_IFSOCK), +#endif +#ifdef O_CREAT + SCR_FS_C(O_CREAT), +#endif +#ifdef O_EXCL + SCR_FS_C(O_EXCL), +#endif + {"UV_FS_O_FILEMAP", 0}, + {"UV_FS_O_TEMPORARY", 0}, + {"UV_FS_O_SHORT_LIVED", 0}, + {"UV_FS_O_SEQUENTIAL", 0}, + {"UV_FS_O_RANDOM", 0}, +#ifdef O_NOCTTY + SCR_FS_C(O_NOCTTY), +#endif +#ifdef O_TRUNC + SCR_FS_C(O_TRUNC), +#endif +#ifdef O_APPEND + SCR_FS_C(O_APPEND), +#endif +#ifdef O_DIRECTORY + SCR_FS_C(O_DIRECTORY), +#endif +#ifdef O_NOATIME + SCR_FS_C(O_NOATIME), +#endif +#ifdef O_NOFOLLOW + SCR_FS_C(O_NOFOLLOW), +#endif +#ifdef O_SYNC + SCR_FS_C(O_SYNC), +#endif +#ifdef O_DSYNC + SCR_FS_C(O_DSYNC), +#endif +#ifdef O_SYMLINK + SCR_FS_C(O_SYMLINK), +#endif +#ifdef O_DIRECT + SCR_FS_C(O_DIRECT), +#endif +#ifdef O_NONBLOCK + SCR_FS_C(O_NONBLOCK), +#endif +#ifdef S_IRWXU + SCR_FS_C(S_IRWXU), +#endif +#ifdef S_IRUSR + SCR_FS_C(S_IRUSR), +#endif +#ifdef S_IWUSR + SCR_FS_C(S_IWUSR), +#endif +#ifdef S_IXUSR + SCR_FS_C(S_IXUSR), +#endif +#ifdef S_IRWXG + SCR_FS_C(S_IRWXG), +#endif +#ifdef S_IRGRP + SCR_FS_C(S_IRGRP), +#endif +#ifdef S_IWGRP + SCR_FS_C(S_IWGRP), +#endif +#ifdef S_IXGRP + SCR_FS_C(S_IXGRP), +#endif +#ifdef S_IRWXO + SCR_FS_C(S_IRWXO), +#endif +#ifdef S_IROTH + SCR_FS_C(S_IROTH), +#endif +#ifdef S_IWOTH + SCR_FS_C(S_IWOTH), +#endif +#ifdef S_IXOTH + SCR_FS_C(S_IXOTH), +#endif +#ifdef F_OK + SCR_FS_C(F_OK), +#endif +#ifdef R_OK + SCR_FS_C(R_OK), +#endif +#ifdef W_OK + SCR_FS_C(W_OK), +#endif +#ifdef X_OK + SCR_FS_C(X_OK), +#endif + {"UV_FS_COPYFILE_EXCL", 1}, + {"COPYFILE_EXCL", 1}, + {"UV_FS_COPYFILE_FICLONE", 2}, + {"COPYFILE_FICLONE", 2}, + {"UV_FS_COPYFILE_FICLONE_FORCE", 4}, + {"COPYFILE_FICLONE_FORCE", 4}, +}; +#undef SCR_FS_C + +static SCR_TL ScrDyn *scr_fs_constants_value; +static void scr_fs_constants_clear(void) { + ScrDyn *value = scr_fs_constants_value; + scr_fs_constants_value = NULL; + scr_dyn_release(value); +} + +ScrDyn *scr_fs_constants_object(void) { + if (scr_fs_constants_value) return scr_dyn_retain(scr_fs_constants_value); + ScrDyn *o = scr_dyn_new_obj_null_proto(); + for (size_t i = 0; i < sizeof scr_fs_constant_table / sizeof scr_fs_constant_table[0]; i++) + scr_os_constant(o, scr_fs_constant_table[i].name, scr_fs_constant_table[i].value); + scr_fs_constants_value = o; + scr_atexit(scr_fs_constants_clear); + return scr_dyn_retain(o); +} diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h index 219726660..57f219ad1 100644 --- a/packages/runtime/src/scr_os.h +++ b/packages/runtime/src/scr_os.h @@ -38,6 +38,8 @@ ScrStr *scr_os_cpus_model(double i); double scr_os_cpus_field(double i, double field); /* os.constants: the per-thread null-prototype constants object (+1). */ ScrDyn *scr_os_constants(void); +/* fs.constants: the target's null-prototype constants object (cached, +1). */ +ScrDyn *scr_fs_constants_object(void); /* The deprecated node:constants module object (scr_os.c): the os groups, * fs.constants and the crypto table the borrowed function answers, merged * and frozen (+1). */ 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..fa104058f 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 ─────────────────────────────────────────────── */ @@ -1864,6 +2020,222 @@ static ScrStr *scr_str_case_conv(const ScrStr *s, int to_lower) { ScrStr *scr_str_to_lower(const ScrStr *s) { return scr_str_case_conv(s, 1); } ScrStr *scr_str_to_upper(const ScrStr *s) { return scr_str_case_conv(s, 0); } +/* ── String.prototype.toLocaleLowerCase / toLocaleUpperCase ──────────── + * V8 decides the case mapping from the FIRST requested locale only + * (probed: ["en", 5] maps like "en", [5] throws, a non-object primitive + * requests nothing). ICU tailors case mapping for Turkic (tr, az: dotted + * and dotless I), Lithuanian (dot above retention) and Greek uppercasing + * (accent removal); every other language uses the default conversion. + * The Turkic rules are SpecialCasing's: lowercase I → ı unless a + * U+0307 follows (then I + U+0307 → i), İ → i; uppercase i → İ. Tags are + * checked against the unicode_locale_id structure; anything this unit + * cannot decide exactly (malformed tags, whose V8 RangeError texts vary, + * objects, lt, el uppercasing, I before other combining marks) throws a + * 'not supported yet' Error rather than guess. Nothing on an existing + * path calls this. */ + +static bool scr_tag_alpha(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!((p[i] >= 'a' && p[i] <= 'z') || (p[i] >= 'A' && p[i] <= 'Z'))) return false; + return true; +} + +static bool scr_tag_digit(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!(p[i] >= '0' && p[i] <= '9')) return false; + return true; +} + +static bool scr_tag_alnum(const char *p, size_t n) { + for (size_t i = 0; i < n; i++) + if (!scr_tag_alpha(p + i, 1) && !scr_tag_digit(p + i, 1)) return false; + return true; +} + +static bool scr_tag_same(const char *a, size_t an, const char *b, size_t bn) { + if (an != bn) return false; + for (size_t i = 0; i < an; i++) { + char x = a[i] >= 'A' && a[i] <= 'Z' ? (char)(a[i] + 32) : a[i]; + char y = b[i] >= 'A' && b[i] <= 'Z' ? (char)(b[i] + 32) : b[i]; + if (x != y) return false; + } + return true; +} + +/* 0 default, 1 Turkic, 2 Lithuanian, 3 Greek; -1 when the tag is outside + * the structural grammar handled here. */ +static int scr_locale_case_rules(const char *tag, size_t len) { + const char *subtags[64]; + size_t lens[64]; + size_t count = 0; + size_t start = 0; + for (size_t i = 0; i <= len; i++) { + if (i < len && tag[i] != '-') continue; + if (i == start || count == 64) return -1; + subtags[count] = tag + start; + lens[count++] = i - start; + start = i + 1; + } + size_t k = 0; + size_t lang = lens[0]; + if (!scr_tag_alpha(subtags[0], lang) || !((lang >= 2 && lang <= 3) || (lang >= 5 && lang <= 8))) + return -1; + k = 1; + if (k < count && lens[k] == 4 && scr_tag_alpha(subtags[k], 4)) k++; + if (k < count && ((lens[k] == 2 && scr_tag_alpha(subtags[k], 2)) || + (lens[k] == 3 && scr_tag_digit(subtags[k], 3)))) + k++; + size_t variants = k; + while (k < count && scr_tag_alnum(subtags[k], lens[k]) && + ((lens[k] >= 5 && lens[k] <= 8) || (lens[k] == 4 && scr_tag_digit(subtags[k], 1)))) { + for (size_t j = variants; j < k; j++) + if (scr_tag_same(subtags[j], lens[j], subtags[k], lens[k])) return -1; + k++; + } + size_t singletons = k; + while (k < count) { + if (lens[k] != 1 || !scr_tag_alnum(subtags[k], 1)) return -1; + for (size_t j = singletons; j < k; j++) + if (lens[j] == 1 && scr_tag_same(subtags[j], 1, subtags[k], 1)) return -1; + bool private_use = subtags[k][0] == 'x' || subtags[k][0] == 'X'; + size_t min = private_use ? 1 : 2; + size_t first = ++k; + while (k < count && lens[k] >= min && lens[k] <= 8 && scr_tag_alnum(subtags[k], lens[k]) && + (private_use || lens[k] > 1)) + k++; + if (k == first) return -1; + if (private_use && k != count) return -1; + } + if (lang > 3) return 0; + char l[3] = {0, 0, 0}; + for (size_t i = 0; i < lang; i++) + l[i] = subtags[0][i] >= 'A' && subtags[0][i] <= 'Z' ? (char)(subtags[0][i] + 32) : subtags[0][i]; + bool two = lang == 2; + if ((two && l[0] == 't' && l[1] == 'r') || (two && l[0] == 'a' && l[1] == 'z') || + (!two && l[0] == 't' && l[1] == 'u' && l[2] == 'r') || + (!two && l[0] == 'a' && l[1] == 'z' && l[2] == 'e')) + return 1; + if ((two && l[0] == 'l' && l[1] == 't') || (!two && l[0] == 'l' && l[1] == 'i' && l[2] == 't')) + return 2; + if ((two && l[0] == 'e' && l[1] == 'l') || (!two && l[0] == 'e' && l[1] == 'l' && l[2] == 'l') || + (!two && l[0] == 'g' && l[1] == 'r' && l[2] == 'e')) + return 3; + return 0; +} + +static ScrStr *scr_locale_case_unsupported(bool upper) { + static const char lower_msg[] = + "String.prototype.toLocaleLowerCase with this locale argument is not supported yet"; + static const char upper_msg[] = + "String.prototype.toLocaleUpperCase with this locale argument is not supported yet"; + /* The unit's existing "SC2020" literal: a separate constant array would + * absorb it (LLVM merges identical constants) and move it out of the + * string section every regex-linking binary keeps. */ + if (upper) scr_throw_error_msg_code(SCR_ERR_ERROR, upper_msg, sizeof upper_msg - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, lower_msg, sizeof lower_msg - 1, "SC2020"); + return NULL; +} + +static ScrStr *scr_turkic_case(const ScrStr *s, bool upper) { + ScrJsonBuf b; + scr_jb_init(&b); + for (size_t i = 0; i < s->len; i++) { + unsigned char c = (unsigned char)s->data[i]; + if (upper && c == 'i') { + scr_jb_putc(&b, (char)0xC4); /* İ */ + scr_jb_putc(&b, (char)0xB0); + continue; + } + if (!upper && c == 'I') { + unsigned char n0 = i + 1 < s->len ? (unsigned char)s->data[i + 1] : 0; + unsigned char n1 = i + 2 < s->len ? (unsigned char)s->data[i + 2] : 0; + if (n0 == 0xCC && n1 == 0x87) { /* I + U+0307 → i */ + scr_jb_putc(&b, 'i'); + i += 2; + continue; + } + /* Another combining mark (U+0300..U+036F) may precede a U+0307. */ + if ((n0 == 0xCC && n1 >= 0x80) || (n0 == 0xCD && n1 >= 0x80 && n1 <= 0xAF)) { + scr_str_release(scr_jb_finish(&b)); + return scr_locale_case_unsupported(upper); + } + scr_jb_putc(&b, (char)0xC4); /* ı */ + scr_jb_putc(&b, (char)0xB1); + continue; + } + if (!upper && c == 0xC4 && i + 1 < s->len && (unsigned char)s->data[i + 1] == 0xB0) { + scr_jb_putc(&b, 'i'); /* İ → i */ + i++; + continue; + } + scr_jb_putc(&b, (char)c); + } + ScrStr *mapped = scr_jb_finish(&b); + ScrStr *result = scr_str_case_conv(mapped, upper ? 0 : 1); + scr_str_release(mapped); + return result; +} + +ScrStr *scr_str_locale_case(const ScrStr *s, ScrDyn *locales, bool upper) { + ScrDyn *owned = NULL; + if (locales->kind == SCR_DYN_TYPED_REF) { + owned = scr_dyn_typed_ref_materialize(locales); + if (!owned) return NULL; + locales = owned; + } + const ScrDyn *tag = NULL; + int rules = 0; + switch (locales->kind) { + case SCR_DYN_NULL: { + static const char msg[] = "Cannot convert undefined or null to object"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + case SCR_DYN_UNDEF: + case SCR_DYN_NUM: + case SCR_DYN_BOOL: + case SCR_DYN_SYMBOL: + case SCR_DYN_BIGINT: + break; + case SCR_DYN_STR: + tag = locales; + break; + case SCR_DYN_ARR: + if (locales->v.arr.len == 0) break; + if (locales->v.arr.presence && !locales->v.arr.presence[0]) { + static const char msg[] = "Language ID should be string or object."; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + scr_dyn_release(owned); + return NULL; + } + tag = locales->v.arr.items[0]; + if (tag->kind != SCR_DYN_STR) { + bool primitive = tag->kind == SCR_DYN_UNDEF || tag->kind == SCR_DYN_NULL || + tag->kind == SCR_DYN_NUM || tag->kind == SCR_DYN_BOOL || + tag->kind == SCR_DYN_BIGINT || tag->kind == SCR_DYN_SYMBOL; + scr_dyn_release(owned); + if (!primitive) return scr_locale_case_unsupported(upper); + static const char msg[] = "Language ID should be string or object."; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + return NULL; + } + break; + default: + scr_dyn_release(owned); + return scr_locale_case_unsupported(upper); + } + if (tag) { + rules = scr_locale_case_rules(tag->v.str->data, tag->v.str->len); + if (rules < 0 || rules == 2 || (rules == 3 && upper)) { + scr_dyn_release(owned); + return scr_locale_case_unsupported(upper); + } + } + scr_dyn_release(owned); + if (rules == 1) return scr_turkic_case(s, upper); + return scr_str_case_conv(s, upper ? 0 : 1); +} + static void *scr_normalize_realloc(void *opaque, void *ptr, size_t size) { (void)opaque; if (!size) { free(ptr); return NULL; } 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_string.c b/packages/runtime/src/scr_string.c index 43cf21b0c..8358658b5 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -6,6 +6,11 @@ #include #include #include +#if defined(__SSE2__) +#include +#elif defined(__ARM_NEON) && defined(__aarch64__) +#include +#endif /* Live heap-string count for the RC audit lane (-DSCR_RC_AUDIT): the test * harness builds with it to prove the emitted retain/release discipline @@ -836,24 +841,93 @@ static __attribute__((noinline)) int scr_str_cmp_u16_units(const ScrStr *a, cons } } +/* The memory index of the first differing byte of two words; x is their + * xor and is not zero. */ +static inline size_t scr_word_mismatch(uint64_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clzll(x) >> 3; +#else + return (size_t)__builtin_ctzll(x) >> 3; +#endif +} + +static inline size_t scr_word32_mismatch(uint32_t x) { +#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + return (size_t)__builtin_clz(x) >> 3; +#else + return (size_t)__builtin_ctz(x) >> 3; +#endif +} + +/* The index of the first byte where the n-byte spans a and b differ, or n + * when they are equal. Spans of at least 8 bytes compare words up to byte + * 32, then 32-byte blocks with SSE2 or NEON where the target has them, + * then words; the last word overlaps bytes already compared. Shorter + * spans compare two overlapping 4-byte halves, or bytes. */ +static inline size_t scr_span_mismatch(const char *a, const char *b, size_t n) { + size_t i = 0; + uint64_t l, r; + if (n < sizeof(uint64_t)) { + if (n >= sizeof(uint32_t)) { + uint32_t l4, r4; + memcpy(&l4, a, 4); + memcpy(&r4, b, 4); + if (l4 != r4) return scr_word32_mismatch(l4 ^ r4); + memcpy(&l4, a + n - 4, 4); + memcpy(&r4, b + n - 4, 4); + return l4 != r4 ? n - 4 + scr_word32_mismatch(l4 ^ r4) : n; + } + while (i < n && a[i] == b[i]) i++; + return i; + } + memcpy(&l, a, 8); + memcpy(&r, b, 8); + if (l != r) return scr_word_mismatch(l ^ r); + size_t head = n < 32 ? n & ~(size_t)7 : 32; + for (i = sizeof(uint64_t); i < head; i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } +#if defined(__SSE2__) + for (; n - i >= 32; i += 32) { + __m128i e0 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i)), + _mm_loadu_si128((const __m128i *)(b + i))); + __m128i e1 = _mm_cmpeq_epi8(_mm_loadu_si128((const __m128i *)(a + i + 16)), + _mm_loadu_si128((const __m128i *)(b + i + 16))); + uint32_t equal = (uint32_t)_mm_movemask_epi8(e0) | (uint32_t)_mm_movemask_epi8(e1) << 16; + if (equal != UINT32_MAX) return i + (size_t)__builtin_ctz(~equal); + } +#elif defined(__ARM_NEON) && defined(__aarch64__) + /* A differing block leaves the loop; the word loop below finds the byte. */ + for (; n - i >= 32; i += 32) { + uint8x16_t e0 = vceqq_u8(vld1q_u8((const uint8_t *)a + i), vld1q_u8((const uint8_t *)b + i)); + uint8x16_t e1 = + vceqq_u8(vld1q_u8((const uint8_t *)a + i + 16), vld1q_u8((const uint8_t *)b + i + 16)); + if (vminvq_u8(vandq_u8(e0, e1)) != 0xff) break; + } +#endif + for (; n - i >= sizeof(uint64_t); i += sizeof(uint64_t)) { + memcpy(&l, a + i, 8); + memcpy(&r, b + i, 8); + if (l != r) return i + scr_word_mismatch(l ^ r); + } + if (i == n) return n; + size_t j = n - sizeof(uint64_t); + memcpy(&l, a + j, 8); + memcpy(&r, b + j, 8); + return l != r ? j + scr_word_mismatch(l ^ r) : n; +} + /* UTF-16 ordering over canonical WTF-8. Equal bytes can be skipped even * inside a multibyte character: unequal continuation bytes preserve code * point order. Differing lead bytes for U+E000..U+FFFF versus a * supplementary character reverse byte order, because the latter starts * with a UTF-16 high surrogate below U+E000. A lone surrogate against a * supplementary character compares their units. */ -int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { - if (a == b) return 0; +__attribute__((noinline)) int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b) { size_t common = a->len < b->len ? a->len : b->len; - size_t i = 0; - while (common - i >= sizeof(uint64_t)) { - uint64_t left, right; - memcpy(&left, a->data + i, sizeof(left)); - memcpy(&right, b->data + i, sizeof(right)); - if (left != right) break; - i += sizeof(left); - } - while (i < common && a->data[i] == b->data[i]) i++; + size_t i = scr_span_mismatch(a->data, b->data, common); if (i == common) return (a->len > b->len) - (a->len < b->len); unsigned char left = (unsigned char)a->data[i]; unsigned char right = (unsigned char)b->data[i]; @@ -866,6 +940,16 @@ int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { return left < right ? -1 : 1; } +/* Byte order differs from UTF-16 order only where both differing bytes are + * at least 0xED, which a proven-ASCII string never has: such pairs compare + * with memcmp. The scan stays out of line in a global function, so program + * builds can import this body. */ +int scr_str_cmp_u16(ScrStr *a, ScrStr *b) { + if (a == b) return 0; + if (scr_str_known_ascii(a) || scr_str_known_ascii(b)) return scr_str_cmp(a, b); + return scr_str_cmp_u16_bytes(a, b); +} + /* ── interned strings ───────────────────────────────────────────────── * The empty string and every single-character ASCII string are immortal * statics (same layout the emitter uses for literals): charAt/slice churn diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h index 425dfaabf..fc2f15562 100644 --- a/packages/runtime/src/scr_string.h +++ b/packages/runtime/src/scr_string.h @@ -290,16 +290,19 @@ ScrStr *scr_str_concat_f64(ScrStr *a, double x); bool scr_str_eq(ScrStr *a, ScrStr *b); -/* memcmp byte order == code-point order (see SEMANTICS.md: diverges from - * JS UTF-16 code-unit order only for non-BMP vs U+E000..U+FFFF). Returns - * <0, 0, >0. */ +/* memcmp byte order == code-point order, which differs from JS UTF-16 + * code-unit order for non-BMP characters against U+E000..U+FFFF and for + * lone surrogates. Returns <0, 0, >0. Relational string operators use + * scr_str_cmp_u16. */ int scr_str_cmp(ScrStr *a, ScrStr *b); /* a.localeCompare(b) with no locales/options: Node's root-locale order for * Latin-script text, code-point order outside it (scr_string.c). */ double scr_str_locale_compare(ScrStr *a, ScrStr *b); -/* ECMAScript string-list ordering: compare UTF-16 code units even though - * ScrStr stores WTF-8. Returns <0, 0, >0. */ +/* ECMAScript string ordering: compare UTF-16 code units even though ScrStr + * stores WTF-8. Returns <0, 0, >0. */ int scr_str_cmp_u16(ScrStr *a, ScrStr *b); +/* scr_str_cmp_u16 for two distinct strings that are not proven ASCII. */ +int scr_str_cmp_u16_bytes(const ScrStr *a, const ScrStr *b); #endif /* SCR_STRING_H */ diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 62354142e..6ed2e09df 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -73,6 +73,10 @@ bool scr_str_ends_with_from(ScrStr *s, ScrStr *needle, double end_position); ScrStr *scr_str_slice(ScrStr *s, double start, double end); /* substring: clamp-and-swap (negatives clamp to 0, start > end swaps). */ ScrStr *scr_str_substring(ScrStr *s, double start, double end); +/* Annex B substr(start, length): start already ToNumber'd (NaN → 0), a + * negative start counts from the end, length absent (has_length false) + * runs to the end. Returns +1. */ +ScrStr *scr_str_substr(ScrStr *s, double start, double length, bool has_length); /* Scalar-only local slices retain their original owner without copying. * Bounds and split-surrogate replacements are snapshotted at construction. */ @@ -111,6 +115,13 @@ ScrStr *scr_str_trim(ScrStr *s); ScrStr *scr_str_to_lower(const ScrStr *s); ScrStr *scr_str_to_upper(const ScrStr *s); ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form); +/* toLocaleLowerCase/toLocaleUpperCase(locales) (scr_regex.c): undefined, + * empty lists, and primitives other than strings use the default case + * conversion; the first requested tag selects the Turkic (tr, az) rules or + * the default ones. Lithuanian, Greek uppercasing and tags outside the + * structural grammar throw a 'not supported yet' Error; null throws Node's + * TypeError. Borrows both; +1 or NULL with an exception pending. */ +ScrStr *scr_str_locale_case(const ScrStr *s, struct ScrDyn *locales, bool upper); /* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh * malloc'd buffer the caller frees, its length in *out_len. Borrows input. */ uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len); diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c index 383b95986..304f2544f 100644 --- a/packages/runtime/src/scr_symbol.c +++ b/packages/runtime/src/scr_symbol.c @@ -181,72 +181,33 @@ void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep, scr_str_release(ib); } -/* Symbol-keyed globals share the symbol unit's link gate. The always-linked - * JSON unit must not depend on optional symbol retain/release functions. */ -typedef struct ScrGlobalSymbolProperty { - ScrSym *key; - ScrDyn *value; - struct ScrGlobalSymbolProperty *next; -} ScrGlobalSymbolProperty; - -static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL; -static SCR_TL bool scr_global_symbols_cleanup_registered = false; - -static void scr_global_symbols_cleanup(void) { - while (scr_global_symbols) { - ScrGlobalSymbolProperty *entry = scr_global_symbols; - scr_global_symbols = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - } -} - +/* Direct `globalThis[symbol]` accesses use the symbol-keyed properties of + * the global object's box (scr_global_object), the storage every other + * spelling of the global object reaches, so all of them see one property. */ ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return scr_dyn_retain(entry->value); - } - return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *k = scr_dyn_new_symbol(key); + ScrDyn *value = scr_dyn_symbol_key_get(scr_global_object(), k, false); + scr_dyn_release(k); + return value; } void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key != key) continue; - ScrDyn *previous = entry->value; - entry->value = scr_dyn_retain(value); - scr_dyn_release(previous); - return; - } - if (!scr_global_symbols_cleanup_registered) { - scr_atexit(scr_global_symbols_cleanup); - scr_global_symbols_cleanup_registered = true; - } - ScrGlobalSymbolProperty *entry = malloc(sizeof *entry); - if (!entry) scr_trap("scriptc: out of memory\n"); - entry->key = scr_sym_retain(key); - entry->value = scr_dyn_retain(value); - entry->next = scr_global_symbols; - scr_global_symbols = entry; + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_symbol_key_set(scr_global_object(), k, value); + scr_dyn_release(k); } bool scr_dyn_global_symbol_has(ScrSym *key) { - for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) { - if (entry->key == key) return true; - } - return false; + ScrDyn *k = scr_dyn_new_symbol(key); + bool present = scr_dyn_has_own_computed(scr_global_object(), k); + scr_dyn_release(k); + return present; } void scr_dyn_global_symbol_delete(ScrSym *key) { - ScrGlobalSymbolProperty **slot = &scr_global_symbols; - while (*slot) { - ScrGlobalSymbolProperty *entry = *slot; - if (entry->key != key) { slot = &entry->next; continue; } - *slot = entry->next; - scr_sym_release(entry->key); - scr_dyn_release(entry->value); - free(entry); - return; - } + ScrDyn *k = scr_dyn_new_symbol(key); + scr_dyn_key_delete_computed(scr_global_object(), k, false); + scr_dyn_release(k); } /* ── fs.Dirent as a dynamic value ──────────────────────────────────── diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index 00e609129..7c5c243d8 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -126,6 +126,7 @@ struct ScrTestCtx { bool passed; /* t.passed: set when the body passes, as Node's pass() does */ bool listed; /* a suite listed in the failing section (a failing root before) */ double start_ms; /* when the test started running; NaN before */ + size_t serial; /* index in scr_test_nodes, getTestContext()'s handle */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -168,6 +169,17 @@ static SCR_TL size_t scr_test_nfibers = 0, scr_test_fibers_cap = 0; /* The module's `mock` tracker: the restore closure of each mock it made. */ static SCR_TL ScrTestHooks scr_test_mocks = {0}; +/* getTestContext(): the test, suite or hook owner whose code is running + * rides the AsyncLocalStorage snapshot under a reserved store id + * (AsyncLocalStorage ids are positive), so it follows a body across its + * awaits and into the async functions it calls, as Node's async context + * does. The entry holds the node's serial, an index in scr_test_nodes + * (borrowed pointers, cleared when a node is freed), so a snapshot that + * outlives its node answers undefined instead of a freed node. */ +#define SCR_TEST_SCOPE_ID (-1.0) +static SCR_TL ScrTestCtx **scr_test_nodes = NULL; +static SCR_TL size_t scr_test_nnodes = 0, scr_test_nodes_cap = 0, scr_test_nodes_live = 0; + #ifdef SCR_RC_AUDIT static SCR_TL long scr_testctx_live = 0; long scr_testctx_live_count(void) { return scr_testctx_live; } @@ -204,6 +216,12 @@ void scr_testctx_release(ScrTestCtx *t) { free(t->children); for (int k = 0; k < 4; k++) scr_test_hooks_free(&t->hooks[k]); scr_test_hooks_free(&t->mocks); + scr_test_nodes[t->serial] = NULL; + if (--scr_test_nodes_live == 0) { + free(scr_test_nodes); + scr_test_nodes = NULL; + scr_test_nnodes = scr_test_nodes_cap = 0; + } #ifdef SCR_RC_AUDIT scr_testctx_live--; #endif @@ -232,12 +250,43 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->timeout = INFINITY; t->concurrency = 1; t->start_ms = NAN; + if (scr_test_nnodes == scr_test_nodes_cap) { + scr_test_nodes_cap = scr_test_nodes_cap ? scr_test_nodes_cap * 2 : 16; + scr_test_nodes = realloc(scr_test_nodes, scr_test_nodes_cap * sizeof *scr_test_nodes); + if (!scr_test_nodes) scr_test_oom(); + } + t->serial = scr_test_nnodes; + scr_test_nodes[scr_test_nnodes++] = t; + scr_test_nodes_live++; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif return t; } +/* Makes `t` the node getTestContext() answers, until the matching + * scr_test_scope_leave. Returns the previous snapshot. */ +static ScrAlsCtx *scr_test_scope_enter(ScrTestCtx *t) { + ScrDyn *serial = scr_dyn_new_num((double)t->serial); + ScrAlsCtx *prev = scr_als_enter(SCR_TEST_SCOPE_ID, serial); + scr_dyn_release(serial); + return prev; +} + +static void scr_test_scope_leave(ScrAlsCtx *prev /*moves*/) { scr_als_restore(prev); } + +/* The node getTestContext() answers (borrowed), or NULL. */ +static ScrTestCtx *scr_test_scope_node(void) { + ScrDyn *serial = scr_als_peek(SCR_TEST_SCOPE_ID); + if (!serial || serial->kind != SCR_DYN_NUM) return NULL; + size_t i = (size_t)serial->v.num; + return i < scr_test_nnodes ? scr_test_nodes[i] : NULL; +} + +bool scr_test_has_context(void) { return scr_test_scope_node() != NULL; } + +ScrTestCtx *scr_test_context(void) { return scr_testctx_retain(scr_test_scope_node()); } + static void scr_test_child_add(ScrTestCtx *parent, ScrTestCtx *child /*moves*/) { if (parent->nchildren == parent->ccap) { parent->ccap = parent->ccap ? parent->ccap * 2 : 4; @@ -393,9 +442,12 @@ static void scr_test_await_timeout(ScrPromise *p, ScrTestCtx *t) { /* Call one stored closure by its lowering-pinned shape; async shapes are * awaited (the runner is a fiber). `ctx` is passed +1 when the body takes - * the TestContext. Returns with the fiber's exception cell holding any - * failure. `timed` is the test whose { timeout } bounds an async body. */ -static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *timed) { + * the TestContext, and `scope` is what getTestContext() answers while it + * runs. Returns with the fiber's exception cell holding any failure. + * `timed` is the test whose { timeout } bounds an async body. */ +static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx *scope, + ScrTestCtx *timed) { + ScrAlsCtx *outer = scr_test_scope_enter(scope); bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = @@ -412,6 +464,7 @@ static void scr_test_call(ScrClosure *cb, int flags, ScrTestCtx *ctx, ScrTestCtx } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); } /* A mock tracker's restoreAll: each mock's restore, in creation order. @@ -434,7 +487,7 @@ static ScrTestHook *scr_test_run_hooks_of(ScrTestCtx *owner, int which, ScrTestC if (scr_exc_pending()) return NULL; /* an earlier hook in the chain threw */ for (size_t i = 0; i < owner->hooks[which].n; i++) { ScrTestHook *h = &owner->hooks[which].v[i]; - scr_test_call(h->cb, h->flags, ctx, NULL); + scr_test_call(h->cb, h->flags, ctx, owner, NULL); if (scr_exc_pending()) return h; /* the hook that threw */ } return NULL; @@ -608,6 +661,7 @@ static void scr_test_cancel_unrun(ScrTestCtx *c) { typedef struct { ScrTestCtx *test; /* owned ref */ ScrTestHooks each; /* the afterEach chain, innermost first; owned closures */ + ScrTestCtx **owners; /* owned refs: each afterEach hook's suite or test */ } ScrTestCancelled; static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { @@ -616,11 +670,13 @@ static void scr_test_cancelled_hooks_entry(ScrFiber *self, void *arg) { ScrTestCtx *c = job->test; scr_await_hop(); /* the spawn runs the fiber at once; start at the next turn */ for (size_t i = 0; i < job->each.n && !scr_exc_pending(); i++) - scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, NULL); + scr_test_call(job->each.v[i].cb, job->each.v[i].flags, c, job->owners[i], NULL); if (scr_exc_pending()) scr_exc_clear(); scr_test_run_hooks_of(c, SCR_HOOK_AFTER, c); if (scr_exc_pending()) scr_exc_clear(); for (int k = 0; k < 4; k++) scr_test_hooks_free(&c->hooks[k]); + for (size_t i = 0; i < job->each.n; i++) scr_testctx_release(job->owners[i]); + free(job->owners); scr_test_hooks_free(&job->each); scr_testctx_release(c); free(job); @@ -636,8 +692,10 @@ static void scr_test_cancelled_hooks(ScrTestCtx *c) { if (job->each.n == job->each.cap) { job->each.cap = job->each.cap ? job->each.cap * 2 : 4; job->each.v = realloc(job->each.v, job->each.cap * sizeof *job->each.v); - if (!job->each.v) scr_test_oom(); + job->owners = realloc(job->owners, job->each.cap * sizeof *job->owners); + if (!job->each.v || !job->owners) scr_test_oom(); } + job->owners[job->each.n] = scr_testctx_retain(p); job->each.v[job->each.n].cb = scr_closure_retain(h->v[i].cb); job->each.v[job->each.n].flags = h->v[i].flags; job->each.v[job->each.n].at = NULL; @@ -775,7 +833,7 @@ static void scr_test_run_node(ScrTestCtx *t) { t->err = scr_exc_take(); failed = true; } else if (t->cb) { - scr_test_call(t->cb, t->flags, t, t); + scr_test_call(t->cb, t->flags, t, t, t); if (t->state != SCR_TS_PENDING) { /* Its parent timed out and cancelled it while the body ran: the * result is reported, its cleanup ran at the cancellation @@ -1703,11 +1761,13 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow if (cb && (int)mode != SCR_TEST_SKIP) { ScrTestCtx *prev = scr_test_parent_get(); scr_test_parent_set(t); + ScrAlsCtx *outer = scr_test_scope_enter(t); /* flag 2: the body takes its SuiteContext (+1, as test bodies do). */ if ((int)flags & SCR_TEST_F_CTX) ((void (*)(ScrClosure *, ScrTestCtx *))cb->fn)(cb, scr_testctx_retain(t)); else ((void (*)(ScrClosure *))cb->fn)(cb); + scr_test_scope_leave(outer); scr_test_parent_set(prev); } scr_closure_release(cb); @@ -1720,6 +1780,7 @@ void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrow * top-level tests and suites (scr_test_fail_unrun). */ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags) { bool takes_ctx = (flags & SCR_TEST_F_CTX) != 0; + ScrAlsCtx *outer = scr_test_scope_enter(root); if (flags & SCR_TEST_F_ASYNC) { ScrPromise *p = takes_ctx @@ -1736,6 +1797,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags * unhandled rejection. */ scr_promise_mark_handled(p); scr_test_root_pending[scr_test_root_npending++] = p; + scr_test_scope_leave(outer); return; } scr_promise_release(p); @@ -1744,6 +1806,7 @@ static void scr_test_root_before_now(ScrTestCtx *root, ScrClosure *cb, int flags } else { ((void (*)(ScrClosure *))cb->fn)(cb); } + scr_test_scope_leave(outer); scr_test_root_before_failed(root); } @@ -1822,7 +1885,7 @@ void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { * each descendant subtest, and after runs once the test finishes. */ void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb /*moves*/, double flags) { if ((int)which == SCR_HOOK_BEFORE) { - scr_test_call(cb, (int)flags, t, NULL); + scr_test_call(cb, (int)flags, t, t, NULL); scr_closure_release(cb); if (scr_exc_pending() && !t->before_err) t->before_err = scr_exc_take(); else if (scr_exc_pending()) scr_exc_clear(); @@ -2279,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); @@ -2394,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 @@ -2409,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_webcrypto.c b/packages/runtime/src/scr_webcrypto.c new file mode 100644 index 000000000..546b24151 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.c @@ -0,0 +1,829 @@ +/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), + * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values + * importKey answers. + * + * This unit is only the WebCrypto layer, modeled on Node 24's + * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument + * conversion, algorithm normalization, the CryptoKey handle, promise + * wrapping and Node's error shapes. The MACs and digests themselves come + * from node:crypto's digest core (scr_crypto_hmac_raw and + * scr_crypto_digest_raw in scr_lib.c), and results settle through the + * deferred crypto queue that node:crypto's callback forms use, so sign, + * verify and digest settle on a later loop turn like Node's thread-pool + * jobs while importKey settles at once. + * + * Every argument and validation failure rejects the returned promise. + * Algorithms Node recognizes but this slice does not implement (RSA, + * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded + * SC2020 Error instead of silently diverging. The compiler links the unit + * only into programs that call these methods (the webcrypto feature). */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +/* ── algorithm registry ─────────────────────────────────────────────── + * The names Node 24 registers per operation (util.js's + * kAlgorithmDefinitions with the conditional algorithms its OpenSSL + * build enables), matched case-insensitively. Only HMAC and the SHA-1/ + * SHA-2 digests are implemented. */ +static const char *const scr_wc_import_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", + "ECDH", "ECDSA", "Ed25519", "Ed448", "HKDF", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "PBKDF2", "RSA-OAEP", "RSA-PSS", + "RSASSA-PKCS1-v1_5", "X25519", "X448", +}; +static const char *const scr_wc_sign_names[] = { + "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", +}; +static const char *const scr_wc_digest_names[] = { + "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", + "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", + "TurboSHAKE256", +}; + +#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) + +/* The implemented hashes: WebCrypto name and node:crypto digest name. */ +static const struct { + const char *name; + const char *native; +} scr_wc_hashes[] = { + {"SHA-1", "sha1"}, + {"SHA-256", "sha256"}, + {"SHA-384", "sha384"}, + {"SHA-512", "sha512"}, +}; + +static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { + size_t n = strlen(name); + if (s->len != n) return false; + for (size_t i = 0; i < n; i++) { + unsigned char a = (unsigned char)s->data[i], b = (unsigned char)name[i]; + if (a >= 'a' && a <= 'z') a = (unsigned char)(a - 32); + if (b >= 'a' && b <= 'z') b = (unsigned char)(b - 32); + if (a != b) return false; + } + return true; +} + +/* The canonical registered spelling of `name`, or NULL. */ +static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { + for (size_t i = 0; i < count; i++) + if (scr_wc_ascii_ieq(name, names[i])) return names[i]; + return NULL; +} + +static const char *scr_wc_native_hash(const char *name) { + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_hashes); i++) + if (strcmp(scr_wc_hashes[i].name, name) == 0) return scr_wc_hashes[i].native; + return NULL; +} + +/* ── errors ────────────────────────────────────────────────────────── */ + +static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; + +static void scr_wc_type_error(const char *code, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +static void scr_wc_type_error(const char *code, const char *fmt, ...) { + char message[512]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(message, sizeof message, fmt, ap); + va_end(ap); + if (n < 0) n = 0; + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, code); +} + +/* An operation or algorithm Node supports that this slice does not. */ +static void scr_wc_unsupported(const char *method, const char *what) { + char message[256]; + int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method, + what); + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); +} + +static void scr_wc_unsupported_algorithm(const char *method, const char *name) { + char what[96]; + snprintf(what, sizeof what, "the %s algorithm", name); + scr_wc_unsupported(method, what); +} + +/* ── Web IDL conversions ────────────────────────────────────────────── + * Each answers NULL/false with the TypeError pending. `prefix` is + * "Failed to execute '' on 'SubtleCrypto'". */ + +static bool scr_wc_is_object(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + return true; + } +} + +/* A typed reference capsule as its plain view; other values retained. */ +static ScrDyn *scr_wc_plain(ScrDyn *v) { + if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); + return scr_dyn_retain(v); +} + +/* BufferSource: the bytes of a TypedArray, DataView, Buffer or + * ArrayBuffer as a u8 view (+1). */ +static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return NULL; + ScrBytes *view = NULL; + if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + view = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or " + "DataView.", + prefix, scr_wc_ordinals[index]); + } + scr_dyn_release(plain); + return view; +} + +/* DOMString conversion (+1). */ +static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } + +static const char *const scr_wc_key_formats[] = { + "raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk", +}; + +/* KeyFormat: the canonical enum spelling, or NULL. */ +static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { + ScrStr *s = scr_wc_dom_string(value); + if (!s) return NULL; + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) { + if (s->len == strlen(scr_wc_key_formats[i]) && + memcmp(s->data, scr_wc_key_formats[i], s->len) == 0) { + scr_str_release(s); + return scr_wc_key_formats[i]; + } + } + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: 1st argument '%.*s' is not a valid enum value of type KeyFormat.", prefix, + (int)(s->len > 200 ? 200 : s->len), s->data); + scr_str_release(s); + return NULL; +} + +/* Node's canonical usage order; the bit is the index. */ +static const char *const scr_wc_usages[] = { + "encrypt", "decrypt", "sign", "verify", + "deriveKey", "deriveBits", "wrapKey", "unwrapKey", + "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", +}; +enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; + +/* sequence over an array: the usage mask (-1 on failure). */ +static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return -1; + if (plain->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.", + prefix, scr_wc_ordinals[index]); + scr_dyn_release(plain); + return -1; + } + long mask = 0; + double n = scr_dyn_arr_len(plain); + for (double i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_arr_at(plain, i); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) { + scr_dyn_release(plain); + return -1; + } + size_t k = 0; + while (k < SCR_WC_COUNT(scr_wc_usages) && + !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) + k++; + if (k == SCR_WC_COUNT(scr_wc_usages)) { + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", + prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), + s->data); + scr_str_release(s); + scr_dyn_release(plain); + return -1; + } + scr_str_release(s); + mask |= 1L << k; + } + scr_dyn_release(plain); + return mask; +} + +/* ── algorithm normalization ────────────────────────────────────────── + * normalizeAlgorithm(algorithm, op) for the members this slice reads: + * the canonical name, and for HMAC imports the hash and length. */ +typedef struct { + const char *name; /* canonical registered name */ + const char *hash; /* HMAC import: the normalized hash name */ + bool has_length; + double length; +} ScrWcAlgorithm; + +static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; + +static void scr_wc_missing_member(const char *dict, const char *member) { + scr_wc_type_error("ERR_MISSING_OPTION", + "%s: passed algorithm cannot be converted to '%s' because '%s' is required in " + "'%s'.", + scr_wc_normalize_prefix, dict, member, dict); +} + +/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ +static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { + if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key)); + return v ? v : scr_dyn_retain(scr_dyn_undefined()); +} + +/* The name an AlgorithmIdentifier carries (+1), with the Algorithm + * dictionary's missing-name TypeError. `dict` names the dictionary the + * value converts to. */ +static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { + if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return NULL; + ScrDyn *name = scr_wc_member(plain, "name"); + scr_dyn_release(plain); + ScrStr *out = NULL; + if (name->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "name"); + else out = scr_wc_dom_string(name); + scr_dyn_release(name); + return out; +} + +/* The digest a HashAlgorithmIdentifier names: canonical name, or NULL + * with NotSupportedError / the SC2020 refusal pending. */ +static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_native_hash(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + return canonical; +} + +/* HmacImportParams over an object: hash (required, a SHA name), then the + * optional [EnforceRange] unsigned long length. */ +static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { + if (!scr_wc_is_object(identifier)) { + scr_wc_missing_member("HmacImportParams", "hash"); + return false; + } + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *hash = scr_wc_member(plain, "hash"); + bool ok = true; + if (hash->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member("HmacImportParams", "hash"); + ok = false; + } + ScrDyn *hash_value = NULL; + if (ok) { + /* HashAlgorithmIdentifier converts to an object or a DOMString, then + * ensureSHA reads the name: a string, or the object's name member. */ + ScrStr *hash_name = NULL; + if (scr_wc_is_object(hash)) { + hash_value = scr_dyn_retain(hash); + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) hash_name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + } else { + hash_name = scr_wc_dom_string(hash); + if (!hash_name) ok = false; + else hash_value = scr_dyn_new_str(hash_name); + } + bool sha = hash_name && hash_name->len >= 3 && + (hash_name->data[0] | 32) == 's' && (hash_name->data[1] | 32) == 'h' && + (hash_name->data[2] | 32) == 'a'; + if (hash_name) scr_str_release(hash_name); + if (ok && !sha) { + scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + ok = false; + } + } + if (ok) { + ScrDyn *length = scr_wc_member(plain, "length"); + if (length->kind != SCR_DYN_UNDEF) { + double x; + if (!scr_dyn_number_coerce_js(length, &x)) ok = false; + else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: length in passed algorithm is not a finite number.", + scr_wc_normalize_prefix); + ok = false; + } else { + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: length in passed algorithm is outside the expected range of 0 to " + "4294967295.", + scr_wc_normalize_prefix); + ok = false; + } else if (x == 0) { + scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); + ok = false; + } else { + out->has_length = true; + out->length = x; + } + } + } + scr_dyn_release(length); + } + if (ok) { + out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + if (!out->hash) ok = false; + } + if (hash_value) scr_dyn_release(hash_value); + scr_dyn_release(hash); + scr_dyn_release(plain); + return ok; +} + +/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with + * the error pending. Only HMAC parameters are read; any other recognized + * algorithm is refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, + bool import, ScrWcAlgorithm *out) { + memset(out, 0, sizeof *out); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + out->name = scr_wc_lookup(names, count, name); + scr_str_release(name); + if (!out->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + return true; +} + +/* ── CryptoKey ───────────────────────────────────────────────────────── + * A secret HMAC key: the bytes, the hash, the bit length, the usage mask + * and the extractable flag. The algorithm and usages getters answer the + * same object on every read (Node caches its clones), built on first + * read. The bytes are cleared when the last reference goes. */ +typedef struct ScrCryptoKey { + size_t rc; + const char *hash; /* WebCrypto hash name */ + double length; /* bits */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; + size_t len; + unsigned char data[]; +} ScrCryptoKey; + +static void *scr_crypto_key_retain(void *p) { + ((ScrCryptoKey *)p)->rc++; + return p; +} + +static void scr_crypto_key_release(void *p) { + ScrCryptoKey *key = p; + if (!key || --key->rc != 0) return; + volatile unsigned char *bytes = key->data; + for (size_t i = 0; i < key->len; i++) bytes[i] = 0; + if (key->algorithm) scr_dyn_release(key->algorithm); + if (key->usage_list) scr_dyn_release(key->usage_list); + free(key); +} + +static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +bool scr_webcrypto_is_crypto_key(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return false; + return scr_crypto_key_of(value) != NULL; +} + +static ScrDyn *scr_wc_cstr(const char *s) { + ScrStr *str = scr_str_new(s, strlen(s)); + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; +} + +static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { + if (!key->algorithm) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + key->algorithm = algorithm; + } + return scr_dyn_retain(key->algorithm); +} + +static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { + if (!key->usage_list) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + key->usage_list = list; + } + return scr_dyn_retain(key->usage_list); +} + +static bool scr_wc_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *scr_crypto_key_get(void *h, const char *name, size_t len) { + ScrCryptoKey *key = h; + if (scr_wc_name_is(name, len, "type")) return scr_wc_cstr("secret"); + if (scr_wc_name_is(name, len, "extractable")) return scr_dyn_new_bool(key->extractable); + if (scr_wc_name_is(name, len, "algorithm")) return scr_crypto_key_algorithm(key); + if (scr_wc_name_is(name, len, "usages")) return scr_crypto_key_usages(key); + return NULL; +} + +/* The four members are prototype getters without setters: an assignment + * in strict code throws Node's TypeError. */ +static bool scr_crypto_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!scr_wc_name_is(name, len, "type") && !scr_wc_name_is(name, len, "extractable") && + !scr_wc_name_is(name, len, "algorithm") && !scr_wc_name_is(name, len, "usages")) + return false; + char message[128]; + int n = snprintf(message, sizeof message, + "Cannot set property %.*s of # which has only a getter", (int)len, + name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *scr_crypto_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)h; + (void)self; + (void)args; + (void)argc; + if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +/* key[Symbol.toStringTag] answers "CryptoKey". */ +static ScrDyn *scr_crypto_key_symbol_get(void *h, const ScrDyn *key) { + (void)h; + if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = description && description->len == 18 && + memcmp(description->data, "Symbol.toStringTag", 18) == 0; + if (description) scr_str_release(description); + return tag ? scr_wc_cstr("CryptoKey") : NULL; +} + +/* No inspect_view: Node's custom inspector prints the members one level + * deeper than a plain object would, which the shared handle inspection + * cannot reproduce, so util.inspect refuses a CryptoKey ('not supported + * yet') rather than printing a different shape. */ +static const ScrDynHandleOps scr_crypto_key_ops = { + .cls = "CryptoKey", + .retain = scr_crypto_key_retain, + .release = scr_crypto_key_release, + .invoke = scr_crypto_key_invoke, + .get = scr_crypto_key_get, + .set = scr_crypto_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = scr_crypto_key_symbol_get, +}; + +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, + double length, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_CRYPTO_KEY, &scr_crypto_key_ops); + } + ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); + if (!key) scr_trap("scriptc: out of memory\n"); + key->rc = 1; + key->hash = hash; + key->length = length; + key->usages = usages; + key->extractable = extractable; + key->algorithm = NULL; + key->usage_list = NULL; + key->len = len; + if (len) memcpy(key->data, data, len); + ScrDyn *box = scr_dyn_new_handle(key, SCR_DYNH_CRYPTO_KEY); + scr_crypto_key_release(key); + return box; +} + +/* ── the operations ─────────────────────────────────────────────────── + * Each takes its argument vector as one dyn array (borrowed) and answers + * a +1 promise. */ + +static ScrPromise *scr_wc_rejected(void) { + ScrPromise *p = scr_promise_new(); + scr_promise_reject_pending(p); + return p; +} + +/* The argument count check every method starts with (after the receiver + * check the compiler's call shape already guarantees). */ +static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { + size_t argc = (size_t)scr_dyn_arr_len(args); + if (argc >= required) return true; + scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix, + required, argc); + return false; +} + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { + for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i); +} + +static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { + for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); +} + +/* importKey(format, keyData, algorithm, extractable, keyUsages): Node's + * conversion order, then macImportKey's checks and importKeySync's empty + * usage check. */ +static ScrDyn *scr_wc_import(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrDyn *result = NULL; + ScrBytes *data = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool jwk = strcmp(format, "jwk") == 0; + /* The JsonWebKey dictionary is not implemented; that format is refused + * after the remaining arguments convert. */ + if (!jwk) { + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + } + if (!scr_wc_is_object(a.v[2])) { + ScrStr *s = scr_wc_dom_string(a.v[2]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm("importKey", algorithm.name); + goto done; + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); + goto done; + } + if (jwk) { + scr_wc_unsupported("importKey", "the 'jwk' key format"); + goto done; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + double length = (double)len * 8; + unsigned char last = view->data[len - 1]; + if (algorithm.has_length) { + double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); + if (bytes != (double)len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + /* A length that is not a whole number of bytes clears the unused + * low bits of the final byte (truncateToBitLength). */ + int extra = (int)fmod(algorithm.length, 8); + if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); + length = algorithm.length; + } + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + goto done; + } + result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, + extractable); + scr_crypto_key_of(result)->data[len - 1] = last; + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 5); + return result; +} + +ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + ScrDyn *key = scr_wc_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* sign(algorithm, key, data) / verify(algorithm, key, signature, data): + * the HMAC over `data`, or NULL with the error pending. *mac_len is the + * digest length. */ +static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char mac[64], + size_t *mac_len, ScrBytes **signature) { + const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'" + : "Failed to execute 'sign' on 'SubtleCrypto'"; + const char *method = verify ? "verify" : "sign"; + size_t n = verify ? 4 : 3; + if (!scr_wc_required(prefix, args, n)) return false; + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + bool ok = false; + ScrBytes *data = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (verify) { + *signature = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!*signature) goto done; + } + data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); + if (!data) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm(method, algorithm.name); + goto done; + } + if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + *mac_len = scr_crypto_hmac_raw(scr_wc_native_hash(key->hash), key->data, key->len, view->data, + (size_t)scr_bytes_byte_len(view), mac); + } + ok = true; +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, n); + return ok; +} + +ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)mac_len); + memcpy(out->data, mac, mac_len); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +/* The verify result's adapter: the deferred value is a dyn boolean. */ +static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + ScrPromise *p; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + ScrDyn *verified = (ScrDyn *)value; + scr_promise_fulfill_bool(p, scr_dyn_truthy(verified)); + scr_dyn_release(verified); +} + +ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + ScrBytes *signature = NULL; + if (!scr_wc_sign_verify(args, true, mac, &mac_len, &signature)) { + if (signature) scr_bytes_release(signature); + return scr_wc_rejected(); + } + bool equal; + { + ScrBytes *view = signature; + SCR_BYTES_SNAPSHOT(view); + /* A signature of another length is simply not valid; equal lengths + * compare in constant time. */ + size_t sig_len = (size_t)scr_bytes_byte_len(view); + equal = sig_len == mac_len; + unsigned char diff = 0; + for (size_t i = 0; equal && i < mac_len; i++) diff |= (unsigned char)(view->data[i] ^ mac[i]); + equal = equal && diff == 0; + } + scr_bytes_release(signature); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(equal), NULL, scr_crypto_promise_callback(p), + scr_wc_settle_verified); + return p; +} + +/* digest(algorithm, data). */ +ScrPromise *scr_webcrypto_digest(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'digest' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrBytes *data = NULL; + ScrBytes *out = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + const char *hash = scr_wc_digest_algorithm(a.v[0], "digest"); + if (!hash) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + unsigned char digest[64]; + size_t len = scr_crypto_digest_raw(scr_wc_native_hash(hash), view->data, + (size_t)scr_bytes_byte_len(view), digest); + out = scr_bytes_new(SCR_BYTES_U8, (double)len); + memcpy(out->data, digest, len); + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 2); + if (!out) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} 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/ci-corpus-costs.mjs b/scripts/ci-corpus-costs.mjs index e934647c8..9f0e8b085 100644 --- a/scripts/ci-corpus-costs.mjs +++ b/scripts/ci-corpus-costs.mjs @@ -1,16 +1,31 @@ #!/usr/bin/env node // Refreshes tests/harness/corpus-costs.json from the logs of the Linux test shards of green CI runs on main. // +// It first folds the per-program cost files (tests/harness/corpus-costs/.json, which changes that add or +// remeasure a program write instead of editing the table) into the table and deletes them. +// // Usage: node scripts/ci-corpus-costs.mjs ... // // Each log is a downloaded "test (Linux, plain|san, i/10)" job log. vitest prints every case slower than its // threshold with its duration; for each program this takes the median of its differential and of its // llvm-differential case over the logs of its flavor, and stores their sum. Programs without a measurement keep // their current value. -import { readFileSync, writeFileSync } from "node:fs"; +import { globSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; const path = new URL("../tests/harness/corpus-costs.json", import.meta.url); const table = JSON.parse(readFileSync(path, "utf8")); +const filesDir = fileURLToPath(new URL("../tests/harness/corpus-costs", import.meta.url)); +let folded = 0; +for (const rel of globSync("**/*.json", { cwd: filesDir })) { + const key = rel.replaceAll("\\", "/").slice(0, -".json".length); + const entry = JSON.parse(readFileSync(join(filesDir, rel), "utf8")); + for (const flavor of ["plain", "san"]) + if (entry[flavor] !== undefined) table[flavor][key] = entry[flavor]; + rmSync(join(filesDir, rel)); + folded++; +} const samples = { plain: {}, san: {} }; for (const log of process.argv.slice(2)) { // Logs keep the terminal color codes (ESC [ ... m); drop them before matching. @@ -40,4 +55,4 @@ for (const flavor of ["plain", "san"]) Object.entries(table[flavor]).sort(([a], [b]) => a.localeCompare(b)), ); writeFileSync(path, JSON.stringify(table, null, 2) + "\n"); -console.log(`updated ${updated} program costs`); +console.log(`updated ${updated} program costs, folded ${folded} per-program cost files`); 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/tests/diagnostics/retired-engine-1112-number-methods.ts b/tests/corpus/1112-number-methods.ts similarity index 73% rename from tests/diagnostics/retired-engine-1112-number-methods.ts rename to tests/corpus/1112-number-methods.ts index c96f9df00..510850dca 100644 --- a/tests/diagnostics/retired-engine-1112-number-methods.ts +++ b/tests/corpus/1112-number-methods.ts @@ -1,7 +1,5 @@ -// Explicit toFixed is static; toPrecision/toString(radix) execute in the -// island. The receiver marshals by value there and the engine auto-boxes -// it, so `this` binds the number exactly as in JS. All stay byte-exact -// differential. +// Number.prototype.toFixed, toPrecision and toString(radix) on typed +// numbers all compile statically and stay byte-exact differential. const n = 1234.5678; console.log(n.toFixed(0), n.toFixed(2), n.toFixed(6)); console.log((0.1 + 0.2).toFixed(17), (-3.7).toFixed(1), (0).toFixed(2)); diff --git a/tests/corpus/1536-string-array-sweep.ts b/tests/corpus/1536-string-array-sweep.ts index 0e5c12ebc..e3d5259ff 100644 --- a/tests/corpus/1536-string-array-sweep.ts +++ b/tests/corpus/1536-string-array-sweep.ts @@ -1,8 +1,7 @@ // The surface-tail sweep trio: string.substring (clamp-and-swap, the // port-suffix idiom), array findIndex (first match or -1, index-aware // callbacks), and the comparator-less sort() on string[] (JS's default -// UTF-16 ordering — a dedicated comparator because relational operators use -// scriptc's documented code-point order). +// UTF-16 code-unit ordering). const localAddr = "127.0.0.1.51823"; const lastColon = localAddr.lastIndexOf("."); console.log(localAddr.substring(lastColon + 1)); diff --git a/tests/corpus/5639-union-arm-by-discriminant.ts b/tests/corpus/5639-union-arm-by-discriminant.ts new file mode 100644 index 000000000..3c29273c5 --- /dev/null +++ b/tests/corpus/5639-union-arm-by-discriminant.ts @@ -0,0 +1,48 @@ +// A record that converts into several arms of a discriminated union takes +// the arm its discriminant names. The `map` callback below builds statement +// records whose `value` is an expression literal; that literal also fits the +// shorter `ref` expression, and the statement also fits `brk` (by dropping +// fields), but the `kind` values pick `get` and `set`, as in Node. +type Expr = + | { kind: "get"; obj: Expr | null; field: string } + | { kind: "ref"; obj: Expr | null } + | { kind: "num"; n: number }; +type Stmt = + | { kind: "set"; obj: Expr; field: string; value: Expr } + | { kind: "brk"; label?: string }; + +function fill(target: Expr, names: string[]): Stmt[] { + return names.map((name) => ({ + kind: "set", + obj: target, + field: name, + value: { kind: "get", obj: target, field: name }, + })); +} + +function describe(s: Stmt): string { + switch (s.kind) { + case "set": + return `set ${s.field} = ${s.value.kind}${s.value.kind === "get" ? `.${s.value.field}` : ""} on ${s.obj.kind}`; + case "brk": + return `brk ${s.label ?? "-"}`; + } +} + +for (const s of fill({ kind: "num", n: 1 }, ["a", "b"])) console.log(describe(s)); + +// The same records with the other discriminant value take the other arm. +function pick(kind: "set" | "brk", label: string): Stmt[] { + return [label].map((l) => + kind === "set" + ? { kind: "set" as const, obj: { kind: "num" as const, n: 2 }, field: l, value: { kind: "ref" as const, obj: null } } + : { kind: "brk" as const, label: l }, + ); +} +console.log(pick("set", "x").map(describe).join("; ")); +console.log(pick("brk", "y").map(describe).join("; ")); + +// A record that fits one arm only, by dropping a field, still takes it. +const extra = { kind: "brk" as const, label: "z", note: 1 }; +const stmts: Stmt[] = [extra]; +console.log(stmts.map(describe).join("; ")); diff --git a/tests/corpus/5640-generic-structural-class-narrowing.ts b/tests/corpus/5640-generic-structural-class-narrowing.ts new file mode 100644 index 000000000..a4144c5df --- /dev/null +++ b/tests/corpus/5640-generic-structural-class-narrowing.ts @@ -0,0 +1,78 @@ +// In a generic function, `node instanceof Identifier || node instanceof +// PrivateIdentifier` narrows `node` to `T & PrivateIdentifier`: Identifier +// has every member PrivateIdentifier has, so TypeScript reduces the union of +// the two intersections to one. The narrowed read still holds either class, +// as in Node, when it is stored, read or called. +abstract class Node { + kind: number; + constructor(kind: number) { + this.kind = kind; + } +} +class Identifier extends Node { + text: string; + flow: number | undefined; + constructor(text: string) { + super(1); + this.text = text; + this.flow = undefined; + } + describe(): string { + return `identifier ${this.text}`; + } +} +class PrivateIdentifier extends Node { + text: string; + constructor(text: string) { + super(2); + this.text = text; + } + describe(): string { + return `private ${this.text}`; + } +} +class Keyword extends Node { + constructor() { + super(3); + } +} + +const names = new Map(); + +function clone(node: T, copy: T): T { + if (node instanceof Identifier || node instanceof PrivateIdentifier) { + const name = names.get(node); + if (name !== undefined) names.set(copy, name + "'"); + } + return copy; +} + +function label(node: T): string { + if (node instanceof Identifier || node instanceof PrivateIdentifier) + return `${node.kind} ${node.text} (${node.describe()})`; + return `${node.kind} -`; +} + +// One instanceof test names one class. +function privateText(node: T): string { + if (node instanceof PrivateIdentifier) return node.text; + return "-"; +} + +const id = new Identifier("a"); +const hash = new PrivateIdentifier("#b"); +const keyword = new Keyword(); +const asNode: Node = id; +names.set(id, "A"); +names.set(hash, "B"); + +console.log(names.get(clone(id, new Identifier("a2")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b2")))); +console.log(names.get(clone(asNode, new Identifier("a3")))); +console.log(names.get(clone(hash, new PrivateIdentifier("#b3")))); +console.log(names.get(clone(keyword, new Keyword()))); +console.log(names.size); + +for (const node of [id, hash, keyword] as Node[]) console.log(label(node)); +console.log(label(id), "|", label(hash)); +console.log(privateText(id), privateText(hash), privateText(hash)); 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/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/6023-string-compare-utf16.ts b/tests/corpus/6023-string-compare-utf16.ts new file mode 100644 index 000000000..c4b9ccf91 --- /dev/null +++ b/tests/corpus/6023-string-compare-utf16.ts @@ -0,0 +1,72 @@ +// Relational string comparisons order strings by UTF-16 code units, as in +// Node: characters above U+FFFF (surrogate pairs) sort before U+E000..U+FFFF, +// lone surrogates sort by their own code unit, and ASCII and other BMP text +// keep their ordinary order. Typed comparisons, sort comparators and +// comparisons of untyped values agree. +const astral = "😀"; // U+1F600: D83D DE00 +const privateUse = "\ue000"; +const last = "\uffff"; +const highSurrogate = "\ud83d"; +const lowSurrogate = "\ude00"; +const hex = (s: string): string => { + const units: string[] = []; + for (let i = 0; i < s.length; i++) units.push(s.charCodeAt(i).toString(16)); + return units.join(" "); +}; + +const pairs: [string, string][] = [ + [astral, last], + [astral, privateUse], + ["\ud7ff", astral], + [highSurrogate, astral], + [lowSurrogate, astral], + [lowSurrogate, last], + ["é", "e"], + ["abc😀", "abc\uffff"], + ["same", "same"], + ["prefix", "prefix😀"], +]; +for (const [a, b] of pairs) + console.log(hex(a), "|", hex(b), a < b, a <= b, a > b, a >= b); + +const words = ["\uffff", "😀", "é", "e", "\ue000", "z", "\ud83d", "A"]; +const byOperator = [...words].sort((x, y) => (x < y ? -1 : x > y ? 1 : 0)); +const byDefault = [...words].sort(); +console.log(byOperator.map(hex).join(", ")); +console.log(byDefault.map(hex).join(", ")); +console.log(byOperator.join("") === byDefault.join("")); + +const untyped: unknown[] = JSON.parse('["\\uffff", "\\ud83d\\ude00", "a"]'); +const [u0, u1] = untyped as string[]; +console.log(u1! < u0!, [...untyped].sort().map((v) => hex(String(v))).join(", ")); + +let smallest = words[0]!; +for (const word of words) if (word < smallest) smallest = word; +console.log(hex(smallest), words.filter((w) => w > "\ud7ff").length); + +// Every pair of these characters after shared prefixes of 0 to 24 bytes, +// ASCII and multibyte, with and without a suffix: the first differing byte +// falls in the first word, the tail word, the first byte or past 16 bytes. +const chars = ["a", "z", "é", "한", "中", "\ud7ff", "\ue000", "\uffff"]; +chars.push(astral, highSurrogate, lowSurrogate, "\u{10ffff}"); +const hashOf = (text: string): number => { + let h = 0; + for (let i = 0; i < text.length; i++) h = (Math.imul(h, 31) + text.charCodeAt(i)) | 0; + return h >>> 0; +}; +for (const unit of ["p", "é", "中"]) { + const rows: string[] = []; + for (let n = 0; n <= 24; n += 3) { + const prefix = unit.repeat(n); + let marks = ""; + for (const x of chars) + for (const y of chars) + for (const suffix of ["", "q", "tail-of-text"]) { + const a = prefix + x + suffix; + const b = prefix + y; + marks += a < b ? "<" : a > b ? ">" : "="; + } + rows.push(`${n}:${marks.split("<").length - 1}:${hashOf(marks)}`); + } + console.log(hex(unit), rows.join(" ")); +} 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/6077-generator-helpers-untyped.js b/tests/corpus/6077-generator-helpers-untyped.js new file mode 100644 index 000000000..381595d9d --- /dev/null +++ b/tests/corpus/6077-generator-helpers-untyped.js @@ -0,0 +1,81 @@ +// Iterator helpers on compiled generators that reach untyped parameters: +// the runtime's helpers step the generator through its own next() and +// close it through return(). +const log = []; +const flush = () => log.splice(0).join(" "); +function* count(n) { + try { + for (let i = 1; i <= n; i++) { + log.push("y" + i); + yield i; + } + return "done"; + } finally { + log.push("closed"); + } +} +function twice(it) { return it.map((x) => x * 2).toArray(); } +console.log(twice(count(3)), flush()); +function firstOver(it, k) { return it.find((x) => x > k); } +console.log(firstOver(count(9), 3), flush()); +function stepper(it) { + const h = it.filter((x) => x % 2 === 1); + return [h.next(), h.next(), h.return(), h.next()]; +} +console.log(stepper(count(9)), flush()); +function sum(it) { return it.reduce((a, b) => a + b, 0); } +console.log(sum(count(4)), flush()); +function product(it) { return it.reduce((a, b) => a * b); } +console.log(product(count(4)), flush()); +function lazy(it) { return it.drop(1).take(2); } +console.log([...lazy(count(9))], flush()); +function each(it) { const out = []; it.forEach((x, i) => out.push(x + ":" + i)); return out; } +console.log(each(count(3)), flush()); +function some(it) { return it.some((x) => x === 2); } +console.log(some(count(5)), flush()); +function every(it) { return it.every((x) => x < 3); } +console.log(every(count(5)), flush()); +function flat(it) { return it.flatMap((x) => [x, -x]).toArray(); } +console.log(flat(count(2)), flush()); +function chain(it) { return it.map((x) => x + 1).filter((x) => x % 2 === 0).map(String).toArray(); } +console.log(chain(count(6)), flush()); +function loop(it) { + const out = []; + for (const x of it.map((x) => x * 10)) { + out.push(x); + if (x >= 20) break; + } + return out; +} +console.log(loop(count(5)), flush()); +function from(it) { return Array.from(it.take(2)); } +console.log(from(count(5)), flush()); +function zeroTake(it) { return it.take(0).toArray(); } +console.log(zeroTake(count(5)), flush()); +function after(it) { const a = it.take(1).toArray(); return [a, it.next()]; } +console.log(after(count(3)), flush()); +function failing(it) { + try { + it.map((x) => { if (x === 2) throw new Error("boom " + x); return x; }).toArray(); + } catch (e) { + return e.message; + } +} +console.log(failing(count(5)), flush()); +function errorOf(run) { try { run(); } catch (e) { return e.name + ": " + e.message; } } +function check(it) { + return [ + errorOf(() => it.map(42)), + errorOf(() => it.take(-1)), + errorOf(() => it.reduce((a, b) => a + b)), + ]; +} +console.log(check(count(0)), flush()); +function kinds(it) { return [typeof it.map, typeof it.toArray, typeof it.next]; } +console.log(kinds(count(1))); +function readNext(it) { const next = it.next; return [next.call(it), next.call(it)]; } +console.log(readNext(count(5)), flush()); +function first(it) { for (const x of it) return x; } +console.log(first(count(3)), flush()); +function closeEarly(it) { it.next(); return it.return(); } +console.log(closeEarly(count(3)), flush()); diff --git a/tests/corpus/6078-destructure-untyped-iterables.js b/tests/corpus/6078-destructure-untyped-iterables.js new file mode 100644 index 000000000..cf8435cba --- /dev/null +++ b/tests/corpus/6078-destructure-untyped-iterables.js @@ -0,0 +1,60 @@ +// Array destructuring of untyped iterables: Map and Set objects, their +// iterators, compiled generators and objects with a Symbol.iterator method +// step through the iterator protocol, and an iterator with values left +// closes. +const m = new Map([["a", 1], ["b", 2], ["c", 3]]); +function two(it) { const [x, y] = it; return [x, y]; } +console.log(two(m), two(new Set([7, 8, 9])), two(m.keys()), two("hey"), two([1])); +function rest(it) { const [x, ...more] = it; return [x, more]; } +console.log(rest(m.values()), rest(new Set([1, 2, 3])), rest(m.entries())); +function* g() { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("finally"); + } +} +console.log(two(g()), rest(g())); +function holes(it) { const [, b, , d] = it; return [b, d]; } +console.log(holes(g())); +function none(it) { const [] = it; return "none"; } +console.log(none(g())); +function* short() { yield "only"; } +console.log(two(short())); +const custom = { + [Symbol.iterator]() { + let i = 0; + return { + next: () => ({ value: i++, done: i > 5 }), + return() { + console.log("return called"); + return {}; + }, + }; + }, +}; +console.log(two(custom)); +const helper = [1, 2, 3, 4].values().map((x) => x * 3); +console.log(two(helper), [...helper]); +for (const bad of [1, null, undefined, {}, true]) { + try { + two(bad); + } catch (e) { + console.log(e.name, e.message); + } +} +const throwing = { + [Symbol.iterator]() { + return { + next() { throw new Error("next failed"); }, + return() { console.log("not called"); return {}; }, + }; + }, +}; +try { + two(throwing); +} catch (e) { + console.log(e.message); +} diff --git a/tests/corpus/6079-generator-helpers-any.ts b/tests/corpus/6079-generator-helpers-any.ts new file mode 100644 index 000000000..f72fd55ef --- /dev/null +++ b/tests/corpus/6079-generator-helpers-any.ts @@ -0,0 +1,24 @@ +// Iterator helpers on generators passed through `any` and `unknown`. +function* letters(): Generator { + yield "a"; + yield "b"; + yield "c"; +} +function upper(it: any): string[] { + return it.map((s: string) => s.toUpperCase()).toArray(); +} +console.log(upper(letters())); +const boxed: unknown = letters(); +console.log((boxed as any).filter((s: string) => s !== "b").toArray()); +const held: any[] = [letters(), letters()]; +console.log(held.map((h) => h.drop(1).toArray())); +function* numbered(): Generator { + yield 1; + yield 2; + return "end"; +} +function firstTwo(it: any): number[] { + return it.take(2).toArray(); +} +const later: any = numbered(); +console.log(firstTwo(numbered()), later.some((n: number) => n > 1)); diff --git a/tests/corpus/6080-take-then-flatmap.ts b/tests/corpus/6080-take-then-flatmap.ts new file mode 100644 index 000000000..1db6edbb4 --- /dev/null +++ b/tests/corpus/6080-take-then-flatmap.ts @@ -0,0 +1,19 @@ +// take(n) followed by flatMap counts n source values: the last value +// taken still flattens in full, and nothing is pulled after it. +const log: string[] = []; +function* gen(): Generator { for (let i = 0; i < 4; i++) { log.push("y" + i); yield i; } } +const flush = (): string => log.splice(0).join(" "); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).toArray(), flush()); +console.log([0, 1, 2, 3].values().take(2).flatMap((x) => [x, x + 10]).toArray()); +console.log(gen().take(3).flatMap((x) => new Set([x, -x])).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).take(3).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).find((v) => v === 10), flush()); +console.log(gen().take(2).flatMap((x) => [x, x + 10]).some((v) => v > 10), flush()); +console.log(gen().filter((x) => x > 0).take(2).flatMap((x) => [x, x]).map((v) => v * 2).toArray(), flush()); +const out: number[] = []; +for (const v of gen().take(2).flatMap((x) => [x, x + 10])) out.push(v); +console.log(out, flush()); +const h = gen().take(1).flatMap((x) => [x, x + 1, x + 2]); +console.log(h.next(), h.next(), h.next(), h.next(), flush()); +console.log(gen().take(0).flatMap((x) => [x]).toArray(), flush()); +console.log(gen().take(2).flatMap((x) => [x]).flatMap((x) => [x, -x]).toArray(), flush()); 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/6200-string-annexb-locale-methods.ts b/tests/corpus/6200-string-annexb-locale-methods.ts new file mode 100644 index 000000000..8c0c2f66b --- /dev/null +++ b/tests/corpus/6200-string-annexb-locale-methods.ts @@ -0,0 +1,33 @@ +// String.prototype.substr, the locale case pair and the Annex B HTML +// methods on typed strings. substr counts a negative start from the end +// and clamps its length; toLocaleLowerCase/toLocaleUpperCase use the +// default case conversion except for the Turkic languages, decided by the +// first requested locale only; the HTML methods escape '"' in attribute +// values. +const s = "Hello, World"; +console.log(s.substr(0), s.substr(7), s.substr(-5), s.substr(-50, 3), s.substr(3, 4)); +console.log(JSON.stringify([s.substr(20), s.substr(2, -1), s.substr(NaN, 2), s.substr(1, Infinity)])); +console.log(JSON.stringify([s.substr(-Infinity, 2), s.substr(1.9, 2.9), "😀ab".substr(1, 2)])); +function tail(from: number, length?: number): string { + return s.substr(from, length); +} +console.log(tail(2), tail(2, 3), tail(-3, undefined)); + +function lower(text: string, locale?: string | string[]): string { + return text.toLocaleLowerCase(locale); +} +function upper(text: string, locale?: string | string[]): string { + return text.toLocaleUpperCase(locale); +} +const sample = "IİiıÍ ΣΑΣ Straße"; +for (const locale of [undefined, "en", "en-US", "de-DE", "TR", "tr-TR", "az", "tur", "zh-Hant-TW", "und", "en-u-co-phonebk", "en-x-tr"]) { + console.log(String(locale), lower(sample, locale), upper(sample, locale)); +} +console.log(lower(sample, ["tr", "en"]), lower(sample, ["en", "tr"]), lower(sample, [])); +console.log(upper("istanbul", ["az"]), lower("DİYARBAKIR"), lower("İx", "tr").length); +console.log("Dž".toLocaleUpperCase(), "ß".toLocaleUpperCase("de"), "ffi".toLocaleUpperCase("en")); + + +console.log(s.anchor('a"b'), s.big(), s.blink(), s.bold(), s.fixed()); +console.log(s.fontcolor("red"), s.fontsize(7), s.italics(), s.link("https://x/?q=\"1\"")); +console.log(s.small(), s.strike(), s.sub(), s.sup(), "".anchor("")); diff --git a/tests/corpus/6201-number-precision-exponential.ts b/tests/corpus/6201-number-precision-exponential.ts new file mode 100644 index 000000000..7c3d4eff8 --- /dev/null +++ b/tests/corpus/6201-number-precision-exponential.ts @@ -0,0 +1,34 @@ +// Number.prototype.toPrecision and toExponential(fractionDigits): the +// exact binary value rounds half up at the requested digit, exponential +// form takes over when the exponent is below -6 or reaches the precision, +// and the non-finite answers come before the digit-range RangeError. +const values = [0, -0, 1, -1, 2.5, -2.5, 0.5, 1.005, 1.45, 123.456, 0.000001234, 0.0000001234, 1e21, 1e-7, 5e-324, 1.7976931348623157e308, 123456789012345680000, 0.1, 99.99, 9.995, 1e23, -1234.5678]; +for (const v of values) { + const p = [1, 2, 3, 5, 10, 21].map((d) => v.toPrecision(d)); + const e = [0, 1, 2, 6, 20].map((d) => v.toExponential(d)); + console.log(String(v), p.join(" "), e.join(" ")); +} +console.log((123.456).toPrecision(), (123.456).toPrecision(undefined), (1 / 3).toPrecision(100)); +console.log((1 / 3).toExponential(100), (2 ** 70).toPrecision(25), (2 ** -30).toExponential(30)); +console.log(NaN.toPrecision(200), Infinity.toPrecision(0), (-Infinity).toExponential(-1), NaN.toExponential(101)); +console.log((15).toPrecision(1.9), (15).toExponential(1.9), (0.00001).toPrecision(1), (1e-6).toPrecision(2)); +for (const bad of [0, 101, -1, Infinity]) { + try { + console.log((1).toPrecision(bad)); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +for (const bad of [-1, 101, Infinity]) { + try { + console.log((1).toExponential(bad)); + } catch (e) { + console.log((e as Error).name, (e as Error).message); + } +} +let n = 0; +for (let i = 1; i < 2000; i++) { + const x = i / 7 + i * 1e-9; + if (x.toPrecision(7) !== Number(x.toPrecision(7)).toPrecision(7)) n++; +} +console.log("stable", n); diff --git a/tests/corpus/6202-dyn-string-number-methods.js b/tests/corpus/6202-dyn-string-number-methods.js new file mode 100644 index 000000000..3f50a4636 --- /dev/null +++ b/tests/corpus/6202-dyn-string-number-methods.js @@ -0,0 +1,92 @@ +// String and Number methods on JS-inferred receivers: string receivers run +// the real method (string-width's codePointAt, camelcase's +// toLocaleLowerCase, toml's substr, pretty-bytes' toPrecision); other +// receivers keep JavaScript's answer for the name, an object's own method +// or Node's TypeError; arrays keep the runtime dispatch of shared names. +function width(text) { + let total = 0; + for (const character of text) { + const code = character.codePointAt(0); + total += code >= 0x1100 ? 2 : 1; + } + return total; +} +console.log(width("abc"), width("古"), width("👍x")); + +function describe(input) { + return [ + input.codePointAt(0), + input.codePointAt(-1), + input.codePointAt("1"), + input.substr(1), + input.substr(-2, 1), + input.toLocaleLowerCase(), + input.toLocaleUpperCase("en-US"), + input.toLocaleLowerCase("tr"), + input.trimLeft(), + input.trimRight(), + input.isWellFormed(), + input.toWellFormed().length, + ]; +} +console.log(JSON.stringify(describe(" Iİ😀 "))); +console.log(JSON.stringify(describe("\ud800Ab"))); +const parsed = JSON.parse('[" I\\u0130\\ud83d\\ude00 ", 1337, 0.000123456, [1, 2, 3]]'); +console.log(JSON.stringify(describe(parsed[0]))); + +function number(value, digits) { + return [value.toPrecision(digits), value.toExponential(digits), value.toPrecision()]; +} +console.log(JSON.stringify(number(1337, 3)), JSON.stringify(number(0.000123456, 2))); +console.log(JSON.stringify(number(-1.5, "4")), JSON.stringify(number(2.5, undefined))); +console.log(JSON.stringify(number(parsed[1], 3)), JSON.stringify(number(parsed[2], parsed[1] - 1335))); + +function prettyBytes(n) { + const units = ["B", "kB", "MB", "GB"]; + const exponent = Math.min(Math.floor(Math.log10(n) / 3), units.length - 1); + return Number((n / 1000 ** exponent).toPrecision(3)) + " " + units[exponent]; +} +console.log(prettyBytes(1337), prettyBytes(100), prettyBytes(12345678)); + +const own = { codePointAt(i) { return "own " + i; }, substr() { return "own substr"; }, toPrecision(d) { return "own " + d; } }; +console.log(describeOwn(own)); +function describeOwn(value) { + return [value.codePointAt(3), value.substr(), value.toPrecision(2)].join("|"); +} +function arrays(list) { + return JSON.stringify([list.at(-1), list.concat([4], 5)]); +} +console.log(arrays([1, 2, 3]), arrays(parsed[3])); + +function attempt(f) { + try { + return String(f()); + } catch (e) { + return e.name + ": " + e.message; + } +} +function fail(value) { + return [ + attempt(() => value.codePointAt(0)), + attempt(() => value.substr(0)), + attempt(() => value.toPrecision(2)), + attempt(() => value.toLocaleLowerCase()), + ].join(" / "); +} +for (const value of [42, "str", undefined, null, {}, parsed[3]]) console.log(fail(value)); +for (const value of JSON.parse('[42, "str", null, {}, true]')) console.log(fail(value)); +console.log(attempt(() => number(1, 200))); +console.log(attempt(() => "x".toLocaleUpperCase(null))); + +// A helper whose parameter the checker leaves untyped answers `string | +// undefined` natively; the guarded receiver still runs String methods. +function afterX(segment) { + const index = segment.indexOf("x"); + return index === -1 ? undefined : segment.slice(index + 1); +} +function tailCode(text) { + const rest = afterX(text); + if (rest === undefined) return -1; + return [rest.codePointAt(0), rest.substr(1), rest.toUpperCase(), rest.slice(1)].join(","); +} +console.log(tailCode("axé😀"), tailCode("abc")); diff --git a/tests/corpus/6203-any-string-number-methods.ts b/tests/corpus/6203-any-string-number-methods.ts new file mode 100644 index 000000000..202365e5e --- /dev/null +++ b/tests/corpus/6203-any-string-number-methods.ts @@ -0,0 +1,15 @@ +// String and Number methods through 'any'-typed values: string and number +// receivers run the real methods, other values keep JavaScript's answer. +const values: any[] = JSON.parse('["Straße İx", 1234.5678, {"substr": 1}, [1, 2]]'); +const [text, num, obj, list] = values; +console.log(text.codePointAt(7), text.substr(-2), text.toLocaleUpperCase(), text.toLocaleLowerCase("az")); +console.log(text.trimRight(), text.isWellFormed(), text.toLocaleLowerCase(["TR"])); +console.log(num.toPrecision(6), num.toExponential(3), num.toPrecision(), num.toExponential()); +console.log(list.at(-1), JSON.stringify(list.concat([3]))); +for (const call of [() => obj.substr(0), () => num.codePointAt(0), () => text.toPrecision(2)]) { + try { + console.log(call()); + } catch (e) { + console.log((e as Error).name + ": " + (e as Error).message); + } +} diff --git a/tests/corpus/6204-crypto-get-random-values.ts b/tests/corpus/6204-crypto-get-random-values.ts new file mode 100644 index 000000000..fae3c4049 --- /dev/null +++ b/tests/corpus/6204-crypto-get-random-values.ts @@ -0,0 +1,36 @@ +// crypto.getRandomValues through node:crypto's export and webcrypto: +// integer typed arrays fill in place and come back as the same object; +// views over 65536 bytes throw QuotaExceededError. (The global spelling and +// the float/DataView TypeMismatchError are 6205's.) +import { getRandomValues, webcrypto } from "node:crypto"; +import * as nodeCrypto from "node:crypto"; + +const bytes = new Uint8Array(4096); +const same = webcrypto.getRandomValues(bytes); +function nonzero(view: Uint8Array): boolean { + for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true; + return false; +} +function nonzeroWords(view: Uint32Array): boolean { + for (let i = 0; i < view.length; i++) if (view[i] !== 0) return true; + return false; +} +console.log(same === bytes, bytes.length, nonzero(bytes)); +const words = new Uint32Array(256); +console.log(getRandomValues(words) === words, nonzeroWords(words)); +const shorts = new Int16Array(8); +console.log(webcrypto.getRandomValues(shorts) === shorts, nodeCrypto.getRandomValues(new Uint8ClampedArray(3)).length); +console.log(nodeCrypto.webcrypto.getRandomValues(new Int8Array(65536)).length, getRandomValues(new Uint8Array(0)).length); +const buf = Buffer.alloc(16); +console.log(Buffer.isBuffer(getRandomValues(buf)), buf.subarray(4, 8).length); + +function attempt(label: string, f: () => unknown): void { + try { + f(); + console.log(label, "ok"); + } catch (e) { + console.log(label, (e as DOMException).name, (e as DOMException).code, (e as Error).message, e instanceof DOMException); + } +} +attempt("big", () => getRandomValues(new Uint8Array(65537))); +attempt("big32", () => webcrypto.getRandomValues(new Uint32Array(16385))); diff --git a/tests/corpus/6205-crypto-get-random-values.js b/tests/corpus/6205-crypto-get-random-values.js new file mode 100644 index 000000000..97d3e67d5 --- /dev/null +++ b/tests/corpus/6205-crypto-get-random-values.js @@ -0,0 +1,38 @@ +// nanoid's fillRandom: crypto.getRandomValues over Buffer windows of an +// untyped helper, plus non-view values that throw TypeMismatchError. +import { getRandomValues } from "node:crypto"; +const GET_RANDOM_LIMIT = 65536; +function fillRandom(buffer) { + let from = 0; + while (from < buffer.length) { + const to = Math.min(from + GET_RANDOM_LIMIT, buffer.length); + crypto.getRandomValues(buffer.subarray(from, to)); + from = to; + } +} +const alphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +function nanoid(size = 21) { + const bytes = Buffer.allocUnsafe(size); + fillRandom(bytes); + let id = ""; + for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63]; + return id; +} +const id = nanoid(); +console.log(id.length, /^[A-Za-z0-9_-]+$/.test(id), nanoid(10).length, nanoid(70000).length); +const filled = globalThis.crypto.getRandomValues(new Uint16Array(3)); +console.log(filled.length); +for (const value of [new Float64Array(1), new Float32Array(2), new DataView(new ArrayBuffer(2)), new Uint8Array(65537), [1, 2], "x", 5, {}]) { + try { + crypto.getRandomValues(value); + console.log("ok"); + } catch (e) { + console.log(e.name, e.message); + } +} +const untyped = JSON.parse("[1]"); +try { + getRandomValues(untyped); +} catch (e) { + console.log(e.name, e.code, e.message); +} diff --git a/tests/corpus/6206-buffer-is-buffer-views.ts b/tests/corpus/6206-buffer-is-buffer-views.ts new file mode 100644 index 000000000..55e9fd49c --- /dev/null +++ b/tests/corpus/6206-buffer-is-buffer-views.ts @@ -0,0 +1,16 @@ +// Buffer.isBuffer over Uint8Array-typed values reads the view's Buffer +// brand: Buffer factories, Buffer subarrays and slices are Buffers, plain +// typed arrays and copies into them are not. +function check(view: Uint8Array): boolean { + return Buffer.isBuffer(view); +} +const plain = new Uint8Array([1, 2, 3]); +const buffer = Buffer.from(plain); +console.log(check(plain), check(buffer), check(Buffer.alloc(2)), check(Buffer.from("hey").subarray(1))); +console.log(check(plain.subarray(1)), check(new Uint8Array(buffer)), check(buffer.slice(1)), check(Buffer.concat([plain]))); +const views: Uint8Array[] = [plain, buffer, new TextEncoder().encode("x"), Buffer.allocUnsafe(1)]; +console.log(views.map((v) => Buffer.isBuffer(v)).join(",")); +function words(view: Uint32Array): boolean { + return Buffer.isBuffer(view); +} +console.log(words(new Uint32Array(2)), Buffer.from(plain).toString("hex"), Buffer.from(views[2]!).toString()); diff --git a/tests/corpus/6207-fs-constants-object.ts b/tests/corpus/6207-fs-constants-object.ts new file mode 100644 index 000000000..860cc4708 --- /dev/null +++ b/tests/corpus/6207-fs-constants-object.ts @@ -0,0 +1,19 @@ +// fs.constants as a value and every member: the target platform's flags, +// file-type bits and permission bits, in Node's order, on a null-prototype +// object whose members are read-only. The access-mode bits keep their +// baked literals. +import * as fs from "node:fs"; +import { constants } from "node:fs"; + +// libuv's Windows-only open flags (0 elsewhere) joined the object in later +// Node 24 releases; the rest is the same in every one. +const windowsOnly = ["UV_FS_O_TEMPORARY", "UV_FS_O_SHORT_LIVED", "UV_FS_O_SEQUENTIAL", "UV_FS_O_RANDOM"]; +console.log(JSON.stringify(Object.entries(fs.constants).filter(([k]) => !windowsOnly.includes(k)))); +console.log(fs.constants.O_RDONLY, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, constants.S_IFMT, constants.S_IFDIR); +console.log(constants.W_OK, fs.constants.F_OK, constants.COPYFILE_EXCL, fs.constants.UV_FS_O_FILEMAP); +const c: unknown = fs.constants; +console.log(typeof c, c === (fs.constants as unknown), Object.getPrototypeOf(c as object) === null, Object.keys(fs.constants).length > 40); +const mode = fs.statSync(".").mode; +console.log((mode & constants.S_IFMT) === constants.S_IFDIR, (mode & fs.constants.S_IRUSR) !== 0); +const descriptor = Object.getOwnPropertyDescriptor(c as object, "O_RDONLY"); +console.log(JSON.stringify(descriptor)); diff --git a/tests/corpus/6208-fs-constants-require.cjs b/tests/corpus/6208-fs-constants-require.cjs new file mode 100644 index 000000000..c8488a8bf --- /dev/null +++ b/tests/corpus/6208-fs-constants-require.cjs @@ -0,0 +1,8 @@ +// graceful-fs's reads of require('fs').constants: own-property probes for +// platform-specific flags and member reads through a stored binding. +var fs = require("fs"); +var constants = fs.constants; +console.log(typeof constants.O_RDONLY, constants.O_RDWR, Object.prototype.hasOwnProperty.call(constants, "O_SYMLINK") === ("O_SYMLINK" in constants), typeof constants.hasOwnProperty); +console.log(Object.prototype.hasOwnProperty.call(constants, "S_IFREG"), constants.O_EXCL > 0, typeof require("fs").constants.S_IFLNK); +var names = Object.keys(constants).filter(function (k) { return k.indexOf("UV_DIRENT_") === 0; }); +console.log(names.join(","), constants.NOT_A_CONSTANT); diff --git a/tests/corpus/6209-buffer-module-alias.cjs b/tests/corpus/6209-buffer-module-alias.cjs new file mode 100644 index 000000000..cec2d06d8 --- /dev/null +++ b/tests/corpus/6209-buffer-module-alias.cjs @@ -0,0 +1,25 @@ +// The buffer module's Buffer through CommonJS bindings — `var Buffer = +// require('buffer').Buffer` and a member of a stored require — is the +// global Buffer constructor, with its statics. +var Buffer = require("buffer").Buffer; +var buffer = require("buffer"); +var B2 = buffer.Buffer; +const B3 = require("node:buffer").Buffer; +console.log(Buffer.from("hi").toString("hex"), Buffer.isBuffer(Buffer.alloc(1)), B2.byteLength("héllo"), B2.isBuffer("x")); +console.log(B3.concat([B2.from("a"), Buffer.from([98])]).toString(), B3.alloc(2, 1).toString("base64")); +function toBuffer(chunk) { + return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); +} +console.log(toBuffer("abc").length, toBuffer(Buffer.from("xyz")).toString()); + +// Buffer.from over a JS-inferred `string | Uint8Array | undefined` result +// dispatches on the runtime kind, Node's TypeError for undefined included. +function make(flag) { + return flag ? "ab" : flag === false ? new Uint8Array([99, 100]) : undefined; +} +console.log(Buffer.from(make(true)).toString(), Buffer.from(make(false)).toString("hex")); +try { + Buffer.from(make(null)); +} catch (e) { + console.log(e.name, e.code); +} diff --git a/tests/corpus/6210-typeof-global-static-members.js b/tests/corpus/6210-typeof-global-static-members.js new file mode 100644 index 000000000..88c3c40a1 --- /dev/null +++ b/tests/corpus/6210-typeof-global-static-members.js @@ -0,0 +1,11 @@ +// ky's feature probes: typeof Node's Web-platform constructor globals and +// their static methods answers "function" without materializing the value. +export const supportsAbortController = typeof globalThis.AbortController === "function"; +export const supportsAbortSignal = + typeof globalThis.AbortSignal === "function" && typeof globalThis.AbortSignal.any === "function"; +console.log(supportsAbortController, supportsAbortSignal); +console.log(typeof AbortSignal.timeout, typeof AbortSignal.abort, typeof globalThis.AbortSignal.any); +console.log(typeof Response.json, typeof Response.error, typeof URL.canParse, typeof Promise.withResolvers); +console.log(typeof AbortSignal, typeof globalThis.URL, typeof Headers, typeof globalThis.TextEncoder, typeof URLSearchParams); +const signal = AbortSignal.abort("stop"); +console.log(signal.aborted, signal.reason); diff --git a/tests/corpus/6260-js-var-dyn-identity.js b/tests/corpus/6260-js-var-dyn-identity.js new file mode 100644 index 000000000..f22ce2888 --- /dev/null +++ b/tests/corpus/6260-js-var-dyn-identity.js @@ -0,0 +1,103 @@ +// JavaScript: a function-scoped `var` initialized with an object that +// already exists (an element of an array built with push, a find result, +// a member of another object) holds that object, not a copy: writes +// through the binding are visible through the array, and `===` holds. +function make(i) { + return { id: i, x: i % 17 }; +} + +function updateInPlace() { + var all = []; + for (var i = 0; i < 3; i++) all.push(make(i)); + var same = 0; + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x += 1; + if (o === all[j]) same++; + } + console.log(JSON.stringify(all), same); +} +updateInPlace(); + +function literalElements() { + var all = []; + for (var i = 0; i < 3; i++) all.push({ id: i, x: i }); + for (var j = 0; j < all.length; j++) { + var o = all[j]; + o.x *= 10; + } + console.log(JSON.stringify(all)); +} +literalElements(); + +function initializerForms(flag) { + var all = []; + all.push({ n: 0, inner: { v: 1 } }); + all.push({ n: 1, inner: { v: 2 } }); + var found = all.find(function (e) { + return e.n === 1; + }); + found.n = 11; + var picked = flag ? all[0] : null; + picked.n = 10; + var either = all[5] || all[0]; + either.inner.v += 100; + var inner = all[1].inner; + inner.v = 20; + var last = all.pop(); + console.log(JSON.stringify(all), last === found, picked === either); + console.log(JSON.stringify(last)); +} +initializerForms(true); + +function arraysAndClosures() { + var grid = []; + grid.push([1, 2]); + var row = grid[0]; + row[0] = 9; + row.push(3); + var list = []; + list.push({ count: 0 }); + var target = list[0]; + var bump = function () { + target.count++; + }; + bump(); + bump(); + console.log(JSON.stringify(grid), JSON.stringify(list), target === list[0]); +} +arraysAndClosures(); + +function redeclaredAndReassigned() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var o = all[0]; + o.x = 10; + var o = all[1]; + o.x = 20; + o = { x: 30 }; + all.push(o); + o.x = 40; + console.log(JSON.stringify(all), o === all[2]); +} +redeclaredAndReassigned(); + +const arrow = () => { + var all = []; + all.push({ x: 1 }); + var o = all[0]; + o.x = 5; + console.log(JSON.stringify(all)); +}; +arrow(); + +// An annotated binding keeps its declared type. +function annotated() { + var all = []; + all.push({ x: 1 }); + /** @type {{ x: number }} */ + var o = all[0]; + console.log(o.x); +} +annotated(); diff --git a/tests/corpus/6261-js-destructure-for-of-identity.js b/tests/corpus/6261-js-destructure-for-of-identity.js new file mode 100644 index 000000000..a8821b3b4 --- /dev/null +++ b/tests/corpus/6261-js-destructure-for-of-identity.js @@ -0,0 +1,46 @@ +// JavaScript: array destructuring (var, let and const) and `for (var ... +// of ...)` bind the existing element objects of an array built with push, +// so writes through the bindings reach the array's objects. +function destructure() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var [first] = all; + first.x = 10; + let [, second] = all; + second.x = 20; + const [again] = all; + again.x += 1; + console.log(JSON.stringify(all), first === all[0], again === first, second === all[1]); +} +destructure(); + +function forVarOf() { + var all = []; + all.push({ x: 1 }); + all.push({ x: 2 }); + var seen = []; + for (var o of all) { + o.x *= 10; + seen.push(o === all[seen.length]); + } + console.log(JSON.stringify(all), seen.join()); + for (let p of all) p.x += 1; + console.log(JSON.stringify(all)); +} +forVarOf(); + +function* tagged(list) { + for (var o of list) { + o.seen = true; + yield o; + } +} +function generators() { + var items = []; + items.push({ seen: false }); + items.push({ seen: false }); + for (var it of tagged(items)) it.extra = items.indexOf(it); + console.log(JSON.stringify(items)); +} +generators(); diff --git a/tests/corpus/6262-js-file-scope-var-identity.js b/tests/corpus/6262-js-file-scope-var-identity.js new file mode 100644 index 000000000..cb8678b5f --- /dev/null +++ b/tests/corpus/6262-js-file-scope-var-identity.js @@ -0,0 +1,40 @@ +// JavaScript: file-scope `var` and `let` bindings initialized with an +// existing object (an element of an array built with push) hold that +// object, including bindings declared in a loop body, by destructuring or +// by for-of, and functions declared above them read the same object. +function show() { + return typeof o === "undefined" ? "unset" : o.x; +} +console.log(show()); + +var all = []; +all.push({ x: 1 }); +all.push({ x: 2 }); + +var o = all[0]; +o.x = 10; +console.log(show(), JSON.stringify(all), o === all[0]); + +var same = 0; +for (var j = 0; j < all.length; j++) { + var item = all[j]; + item.x += 5; + if (item === all[j]) same++; +} +console.log(JSON.stringify(all), same); + +var [head] = all; +head.x = 100; +for (var each of all) { + each.x += 1; + if (each === all[1]) console.log("last", each.x); +} +console.log(JSON.stringify(all), head === o); + +let current = all[1]; +current.x = 7; +function touch() { + current.x++; +} +touch(); +console.log(JSON.stringify(all), current === all[1]); diff --git a/tests/corpus/6263-js-factory-return-identity.cjs b/tests/corpus/6263-js-factory-return-identity.cjs new file mode 100644 index 000000000..eb3f77557 --- /dev/null +++ b/tests/corpus/6263-js-factory-return-identity.cjs @@ -0,0 +1,56 @@ +// JavaScript: an object a function returns is the object the function +// holds elsewhere (a cache entry, a registry element). Bindings that +// receive it and array or object literals built from those bindings keep +// that object, so writes through them reach the cache and registry. +'use strict'; +const cache = { n: 0 }; +function get() { + return cache; +} + +const registry = []; +function make(name) { + const o = { name: name, hits: 0 }; + registry.push(o); + return o; +} + +function identity(v) { + return v; +} +function wrapped(i) { + return identity({ id: i, x: i % 17 }); +} + +function main() { + var a = get(); + a.n = 5; + console.log(cache.n, a === cache); + var list = [a]; + list[0].n = 6; + var holder = { item: a }; + holder.item.n += 1; + console.log(cache.n, list[0] === cache, holder.item === cache); + + var x = make("x"); + let y = make("y"); + const z = make("z"); + x.hits++; + y.hits += 2; + z.hits += 3; + var group = [x, y, z]; + for (var j = 0; j < group.length; j++) group[j].hits *= 10; + console.log(JSON.stringify(registry), group[0] === registry[0]); + + var all = []; + for (var i = 0; i < 4; i++) all.push(wrapped(i)); + var sum = 0; + for (var r = 0; r < 3; r++) + for (var k = 0; k < all.length; k++) { + var e = all[k]; + e.x += 1; + sum += e.x; + } + console.log(all.length, sum, JSON.stringify(all.slice(0, 2))); +} +main(); diff --git a/tests/corpus/6291-webcrypto-hmac-sign-verify.ts b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts new file mode 100644 index 000000000..ddb480a4d --- /dev/null +++ b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts @@ -0,0 +1,94 @@ +// WebCrypto HMAC through the crypto global: importKey('raw') for SHA-1 and +// SHA-2 keys, sign and verify (a wrong or short signature answers false), +// the CryptoKey members, instanceof CryptoKey, the key sources importKey +// and sign accept, and that results settle asynchronously. +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("header.payload"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const key = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], +); +console.log(key.type, key.extractable, key.usages.join(",")); +const algorithm = key.algorithm as HmacKeyAlgorithm; +console.log(algorithm.name, algorithm.length, algorithm.hash.name); +console.log(key instanceof CryptoKey, key.algorithm === key.algorithm, key.usages === key.usages); + +const signature = await crypto.subtle.sign("HMAC", key, data); +console.log(signature.byteLength, hex(signature)); +console.log(await crypto.subtle.verify("HMAC", key, signature, data)); +console.log(await crypto.subtle.verify({ name: "HMAC" }, key, signature, encoder.encode("header.payloaD"))); +const tampered = new Uint8Array(signature); +tampered[0] ^= 1; +console.log(await crypto.subtle.verify("HMAC", key, tampered, data)); +console.log(await crypto.subtle.verify("HMAC", key, signature.slice(0, 16), data)); +console.log(await crypto.subtle.verify("HMAC", key, new Uint8Array(0), data)); + +for (const hash of ["SHA-1", "SHA-384", "SHA-512"]) { + const k = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash }, true, ["sign"]); + const a = k.algorithm as HmacKeyAlgorithm; + const mac = await crypto.subtle.sign({ name: "hmac" }, k, data); + console.log(hash, a.hash.name, a.length, k.extractable, k.usages.join(","), mac.byteLength, hex(mac)); +} + +// Names normalize case-insensitively; usages come back in Node's order. +const lower = await crypto.subtle.importKey( + "raw", + secret, + { name: "hmac", hash: { name: "sha-256" } }, + true, + ["verify", "sign"], +); +const lowerAlgorithm = lower.algorithm as HmacKeyAlgorithm; +console.log(lowerAlgorithm.name, lowerAlgorithm.hash.name, lower.usages.join(",")); + +// An explicit length in bits must match the key bytes. +const sized = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256", length: secret.length * 8 }, + false, + ["verify"], +); +console.log((sized.algorithm as HmacKeyAlgorithm).length, sized.usages.join(",")); + +// Key and data sources: ArrayBuffer, Buffer, DataView and subarray views. +const fromBuffer = await crypto.subtle.importKey( + "raw", + secret.slice().buffer, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(hex(await crypto.subtle.sign("HMAC", fromBuffer, Buffer.from("header.payload"))) === hex(signature)); +const padded = encoder.encode("xxheader.payloadyy"); +console.log(hex(await crypto.subtle.sign("HMAC", key, padded.subarray(2, 16))) === hex(signature)); +console.log(hex(await crypto.subtle.sign("HMAC", key, new DataView(data.buffer))) === hex(signature)); + +// The data is read when sign is called. +const mutable = encoder.encode("header.payload"); +const pending = crypto.subtle.sign("HMAC", key, mutable); +mutable[0] = 0; +console.log(hex(await pending) === hex(signature)); + +// Results settle after the synchronous code that started them. +const order: string[] = []; +const signed = crypto.subtle.sign("HMAC", key, data).then(() => { + order.push("sign"); +}); +const imported = crypto.subtle + .importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + .then(() => { + order.push("importKey"); + }); +order.push("sync"); +await Promise.all([signed, imported]); +console.log(order.join(",")); diff --git a/tests/corpus/6292-webcrypto-hmac-errors.ts b/tests/corpus/6292-webcrypto-hmac-errors.ts new file mode 100644 index 000000000..95e33ae84 --- /dev/null +++ b/tests/corpus/6292-webcrypto-hmac-errors.ts @@ -0,0 +1,83 @@ +// WebCrypto HMAC failures reject with Node's TypeErrors (argument count, +// enum values, buffer sources, CryptoKey arguments, required members) and +// DOMExceptions (usages, key length, algorithm names, key access). +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("payload"); + +function report(label: string, error: Error): void { + if (error instanceof DOMException) { + const dom = error as DOMException; + console.log(label, dom.name, dom.code, dom.message, "DOMException"); + } else { + const coded = error as Error & { code?: string }; + console.log(label, coded.name, coded.code, coded.message); + } +} + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + report(label, e as Error); + } +} + +async function attemptVerify(label: string, run: () => Promise): Promise { + try { + console.log(label, "resolved", await run()); + } catch (e) { + report(label, e as Error); + } +} + +const signOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const verifyOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-384" }, false, ["verify"]); + +await attempt("usage", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "encrypt"]), +); +await attempt("empty usages", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, []), +); +await attempt("zero-length key", () => + crypto.subtle.importKey("raw", new Uint8Array(0), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("length mismatch", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 64 }, false, ["sign"]), +); +await attempt("length zero", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 0 }, false, ["sign"]), +); +await attempt("missing hash", () => + crypto.subtle.importKey("raw", secret, "HMAC", false, ["sign"]), +); +await attempt("md5 hash", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "MD5" }, false, ["sign"]), +); +await attempt("unknown sha", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-999" }, false, ["sign"]), +); +await attempt("unknown algorithm", () => + crypto.subtle.importKey("raw", secret, { name: "NOPE", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("pkcs8 format", () => + crypto.subtle.importKey("pkcs8", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("string key", () => + crypto.subtle.importKey("raw", "secret" as unknown as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); + +await attempt("sign with verify key", () => crypto.subtle.sign("HMAC", verifyOnly, data)); +await attemptVerify("verify with sign key", () => crypto.subtle.verify("HMAC", signOnly, new Uint8Array(32), data)); +await attempt("sign without key", () => + crypto.subtle.sign("HMAC", secret as unknown as CryptoKey, data), +); +await attempt("sign unknown algorithm", () => crypto.subtle.sign("HMAC-SHA256", signOnly, data)); +await attempt("sign string data", () => + crypto.subtle.sign("HMAC", signOnly, "payload" as unknown as BufferSource), +); +await attemptVerify("verify string signature", () => + crypto.subtle.verify("HMAC", verifyOnly, "sig" as unknown as BufferSource, data), +); diff --git a/tests/corpus/6293-webcrypto-digest.ts b/tests/corpus/6293-webcrypto-digest.ts new file mode 100644 index 000000000..ae107c6f0 --- /dev/null +++ b/tests/corpus/6293-webcrypto-digest.ts @@ -0,0 +1,35 @@ +// crypto.subtle.digest over the SHA-1 and SHA-2 hashes: string and object +// algorithm identifiers, typed-array, Buffer and ArrayBuffer input, and +// Node's rejections for unknown names and non-buffer data. +const encoder = new TextEncoder(); +const input = encoder.encode("scriptc digest input"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +for (const name of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const digest = await crypto.subtle.digest(name, input); + console.log(name, digest.byteLength, hex(digest)); +} +console.log(hex(await crypto.subtle.digest({ name: "sha-256" }, Buffer.from("scriptc digest input")))); +console.log(hex(await crypto.subtle.digest("SHA-256", input.slice().buffer))); +console.log(hex(await crypto.subtle.digest("SHA-256", new Uint8Array(0)))); + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + if (e instanceof DOMException) { + const error = e as DOMException; + console.log(label, error.name, error.code, error.message, "DOMException"); + } else { + const error = e as Error & { code?: string }; + console.log(label, error.name, error.code, error.message); + } + } +} + +await attempt("md5", () => crypto.subtle.digest("MD5", input)); +await attempt("string data", () => crypto.subtle.digest("SHA-256", "text" as unknown as BufferSource)); diff --git a/tests/corpus/6294-webcrypto-js-values.mjs b/tests/corpus/6294-webcrypto-js-values.mjs new file mode 100644 index 000000000..93071b39d --- /dev/null +++ b/tests/corpus/6294-webcrypto-js-values.mjs @@ -0,0 +1,52 @@ +// WebCrypto from JavaScript: keys and algorithms carried as dynamic +// values, the CryptoKey brand checks libraries use (Symbol.toStringTag, +// Object.prototype.toString and instanceof), JSON.stringify of a key, and +// rejections for missing arguments and non-key values. +const encoder = new TextEncoder(); +const secret = encoder.encode("dynamic-secret"); +const table = { + HS256: { name: "HMAC", hash: "SHA-256" }, + HS512: { name: "HMAC", hash: "SHA-512" }, +}; + +async function importFor(alg, usage) { + const params = table[alg]; + return crypto.subtle.importKey("raw", secret, params, false, [usage]); +} + +const key = await importFor("HS256", "sign"); +console.log(key[Symbol.toStringTag], Object.prototype.toString.call(key), String(key)); +console.log(key instanceof CryptoKey, secret instanceof CryptoKey, {} instanceof CryptoKey); +console.log(JSON.stringify(key), typeof key, key.type, key.algorithm.hash.name, key.usages.includes("sign")); +const sig = new Uint8Array(await crypto.subtle.sign(table.HS256, key, encoder.encode("a.b"))); +console.log(sig.length, Buffer.from(sig).toString("base64url")); +const verifier = await importFor("HS256", "verify"); +let verified = false; +try { + verified = await crypto.subtle.verify(table.HS256, verifier, sig, encoder.encode("a.b")); +} catch {} +console.log(verified); +const other = await importFor("HS512", "verify"); +console.log(await crypto.subtle.verify(table.HS512, other, sig, encoder.encode("a.b"))); + +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("too few", () => crypto.subtle.sign("HMAC", key)); +await attempt("import too few", () => crypto.subtle.importKey("raw", secret, table.HS256, false)); +await attempt("verify too few", () => crypto.subtle.verify("HMAC", key, sig)); +await attempt("digest too few", () => crypto.subtle.digest("SHA-256")); +const lookalike = JSON.parse('{"type":"secret","usages":["sign"]}'); +await attempt("not a key", () => crypto.subtle.sign("HMAC", lookalike, secret)); +await attempt("bad format", () => crypto.subtle.importKey("nope", secret, table.HS256, false, ["sign"])); +await attempt("bad usage", () => crypto.subtle.importKey("raw", secret, table.HS256, false, ["bogus"])); +await attempt("usages not a sequence", () => crypto.subtle.importKey("raw", secret, table.HS256, false, "sign")); +await attempt("algorithm without name", () => crypto.subtle.sign({ hash: "SHA-256" }, key, secret)); +await attempt("hash object without name", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: {} }, false, ["sign"]), +); diff --git a/tests/corpus/6295-jose-hs256/main.mjs b/tests/corpus/6295-jose-hs256/main.mjs new file mode 100644 index 000000000..a8a669a57 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -0,0 +1,103 @@ +// The real jose@6.2.12 HS256 path: SignJWT and jwtVerify from jose's +// public jose/jwt/sign, jose/jwt/verify and jose/errors entry points, over +// crypto.subtle importKey, sign and verify. node_modules/jose vendors the +// dist/webapi files those entry points load and their declarations, +// unmodified (MIT, LICENSE.md), with a package.json that exports only +// those entry points. +import { SignJWT } from "jose/jwt/sign"; +import { jwtVerify } from "jose/jwt/verify"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-hs256-shared-secret"); +const now = new Date(1_800_000_000_000); +const iat = 1_799_999_000; + +async function sign(claims, alg = "HS256", key = secret) { + const jwt = new SignJWT(claims); + jwt.setProtectedHeader({ alg, typ: "JWT" }); + jwt.setIssuedAt(iat); + jwt.setIssuer("urn:scriptc:issuer"); + jwt.setAudience("urn:scriptc:audience"); + jwt.setSubject("alice"); + return jwt.sign(key); +} + +async function verify(label, token, options = {}, key = secret) { + try { + const { payload, protectedHeader } = await jwtVerify(token, key, { + algorithms: ["HS256"], + currentDate: now, + ...options, + }); + console.log(label, "ok", JSON.stringify(protectedHeader), JSON.stringify(payload)); + } catch (e) { + console.log(label, e.code, e.message, e instanceof errors.JOSEError, e.name, e.claim, e.reason); + } +} + +const token = await sign({ role: "admin", exp: iat + 3600 }); +console.log(token); +await verify("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await verify("bytes token", encoder.encode(token)); +await verify("wrong secret", token, {}, encoder.encode("another-secret")); +const [header, body, signature] = token.split("."); +const forged = Buffer.from(JSON.stringify({ role: "root", sub: "mallory" })).toString("base64url"); +await verify("tampered payload", `${header}.${forged}.${signature}`); +await verify("tampered signature", `${header}.${body}.${signature.slice(0, -2)}AA`); +await verify("malformed", "not-a-jwt"); +await verify("bad signature encoding", `${header}.${body}.***`); +await verify("expired", token, { currentDate: new Date((iat + 7200) * 1000) }); +await verify("clock tolerance", token, { currentDate: new Date((iat + 3610) * 1000), clockTolerance: 30 }); +await verify("not yet valid", await sign({ nbf: iat + 600 })); +await verify("issuer", token, { issuer: "urn:other" }); +await verify("audience", token, { audience: "urn:other" }); +await verify("required claim", token, { requiredClaims: ["jti"] }); +await verify("alg not allowed", token, { algorithms: ["HS512"] }); +await verify("max token age", token, { maxTokenAge: 600 }); + +const hs384 = await sign({ scope: "read" }, "HS384"); +await verify("hs384", hs384, { algorithms: ["HS384"] }); +const hs512 = await sign({ scope: "write" }, "HS512"); +await verify("hs512", hs512, { algorithms: ["HS256", "HS512"] }); +await verify("hs512 as hs256 only", hs512); +await verify("none", `${Buffer.from('{"alg":"none"}').toString("base64url")}.${body}.`, { algorithms: ["none"] }); + +// The same key imported once and reused as a CryptoKey. +const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); +await verify("crypto key", token, {}, cryptoKey); +const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]); +await verify("wrong key hash", token, {}, signingKey); + +// Chained builder calls: jose's declarations type every setter's result as +// `this`, implemented by the JWTClaimsBuilder base SignJWT extends. +const chained = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience(["urn:scriptc:audience", "urn:other"]) + .setSubject("bob") + .setJti("jti-2") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 60) + .sign(secret); +console.log(chained); +await verify("chained", chained, { audience: "urn:scriptc:audience" }); + +// String durations: jose's secs() parses them. +for (const maxTokenAge of ["10 minutes", "1 hour", "1.5h", "2 days", "+20m", "1 week", "1y", "900 secs", "3 HOURS", "2h ago", "-2h ago", "10 fortnights", ""]) { + await verify(`max token age ${JSON.stringify(maxTokenAge)}`, token, { maxTokenAge }); +} +for (const clockTolerance of ["30s", "5 sec", "1 min from now", "1 min ago"]) { + await verify(`clock tolerance ${clockTolerance}`, token, { currentDate: new Date((iat + 3620) * 1000), clockTolerance }); +} +const relative = new SignJWT({ role: "reader" }); +relative.setProtectedHeader({ alg: "HS256" }).setIssuedAt("-1 minute").setExpirationTime("2h"); +const claims = JSON.parse(Buffer.from((await relative.sign(secret)).split(".")[1], "base64url").toString()); +const clock = Math.floor(Date.now() / 1000); +console.log(Math.abs(claims.exp - clock - 7200) <= 2, Math.abs(claims.iat - clock + 60) <= 2); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (e) { + console.log(e.name, e.message); +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/package.json b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json new file mode 100644 index 000000000..bcb353430 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json @@ -0,0 +1,17 @@ +{ + "name": "jose", + "version": "6.2.12", + "license": "MIT", + "type": "module", + "sideEffects": false, + "exports": { + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + } + } +} diff --git a/tests/corpus/6296-array-position-patterns.ts b/tests/corpus/6296-array-position-patterns.ts new file mode 100644 index 000000000..8c55b6152 --- /dev/null +++ b/tests/corpus/6296-array-position-patterns.ts @@ -0,0 +1,23 @@ +// Object patterns over arrays that name positions and `length` (jose's +// `const { 0: header, 1: payload, 2: signature, length } = jws.split(".")`): +// each position is the element or undefined past the end, defaults apply +// to missing positions, and `length` is the array's length. +const { 0: header, 1: payload, 2: signature, length } = "aaa.bbb.ccc".split("."); +console.log(header, payload, signature, length); + +const short = "aaa.bbb".split("."); +const { 0: first, 2: third, length: count } = short; +console.log(first, third === undefined, count); + +const { 1: second = "fallback", 3: fourth = "missing" } = short; +console.log(second, fourth); + +function parts(values: number[]): string { + const { 0: a, 1: b, length: n } = values; + return `${a}:${b}:${n}`; +} +console.log(parts([1, 2, 3]), parts([7])); + +let renamed = ""; +for (const { 0: key, 1: value } of [["x", "1"], ["y", "2"]]) renamed += `${key}=${value};`; +console.log(renamed); diff --git a/tests/corpus/6297-library-runtime-idioms.mjs b/tests/corpus/6297-library-runtime-idioms.mjs new file mode 100644 index 000000000..11d5d5e67 --- /dev/null +++ b/tests/corpus/6297-library-runtime-idioms.mjs @@ -0,0 +1,34 @@ +// Runtime idioms from jose's dist build: the Uint8Array base64/hex +// feature probes (absent in Node 24), String.fromCharCode.apply over byte +// chunks and number lists (the btoa fallback), and structuredClone of a +// parameter whose default is `{}` called with differently shaped objects. +console.log(typeof Uint8Array.prototype.toBase64, typeof Uint8Array.prototype.toHex); +console.log(typeof Uint8Array.prototype.setFromBase64, typeof Uint8Array.prototype.setFromHex); + +function encodeBase64(input) { + if (Uint8Array.prototype.toBase64) return input.toBase64(); + const CHUNK_SIZE = 4, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + return btoa(arr.join("")); +} +console.log(encodeBase64(new TextEncoder().encode("hello, base64 chunks"))); +console.log(encodeBase64(new Uint8Array([0, 255, 128, 1, 2]))); + +const bytes = new Uint8Array([104, 105]); +console.log(String.fromCharCode.apply(null, bytes), String.fromCharCode.apply(undefined, [72, 73])); +const untyped = JSON.parse("[79, 75]"); +console.log(String.fromCharCode.apply(null, untyped), String.fromCharCode.apply(null, []).length); + +let store; +class Builder { + constructor(payload = {}) { + (store ||= new WeakMap()).set(this, structuredClone(payload)); + } + claims() { + return JSON.stringify(store.get(this)); + } +} +console.log(new Builder({ role: "admin", exp: 5 }).claims()); +console.log(new Builder({ scope: "read" }).claims()); +console.log(new Builder().claims()); diff --git a/tests/corpus/6331-jose-root-entry/main.ts b/tests/corpus/6331-jose-root-entry/main.ts new file mode 100644 index 000000000..598154040 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/main.ts @@ -0,0 +1,110 @@ +// The real jose@6.2.12 through its root entry, from TypeScript, the way a +// Next.js proxy uses it: `import { jwtVerify, errors } from "jose"`. +// node_modules/jose vendors the published package's dist/ (every webapi +// module the root entry loads, and its declarations), package.json and +// LICENSE.md unmodified (MIT). Loading the root runs jwks/remote.js, which +// reads Node's navigator global at module load. Covers chained SignJWT +// builders, string durations (jose's secs()), destructured jwtVerify +// results and `instanceof errors.JOSEError` brand checks. +import { SignJWT, jwtVerify, errors, cryptoRuntime, decodeJwt } from "jose"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-root-shared-secret"); +const now = 1_800_000_000; +const iat = 1_799_999_000; + +interface Options { + issuer?: string; + audience?: string; + algorithms?: string[]; + at?: number; + maxTokenAge?: string; + clockTolerance?: string; +} + +async function check(label: string, token: string, options: Options = {}): Promise { + try { + const { payload, protectedHeader } = await jwtVerify(token, secret, { + algorithms: options.algorithms ?? ["HS256"], + currentDate: new Date((options.at ?? now) * 1000), + issuer: options.issuer, + audience: options.audience, + maxTokenAge: options.maxTokenAge, + clockTolerance: options.clockTolerance, + }); + const role = payload.role; + console.log(label, "ok", protectedHeader.alg, payload.sub ?? null, role, payload.exp ?? null); + } catch (error) { + if (error instanceof errors.JOSEError) { + console.log(label, error.code, error.message, error instanceof errors.JWTExpired); + } else if (error instanceof TypeError) { + console.log(label, "TypeError", error.message); + } else { + throw error; + } + } +} + +console.log(cryptoRuntime, errors.JWTExpired.code, errors.JWSSignatureVerificationFailed.code); + +const token = await new SignJWT({ role: "admin" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience("urn:scriptc:audience") + .setSubject("alice") + .setJti("jti-1") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 3600) + .sign(secret); +console.log(token); +console.log(JSON.stringify(decodeJwt(token))); + +await check("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await check("issuer", token, { issuer: "urn:other" }); +await check("audience", token, { audience: "urn:other" }); +await check("expired", token, { at: iat + 7200 }); +await check("wrong secret", await new SignJWT({ role: "admin" }).setProtectedHeader({ alg: "HS256" }).sign(encoder.encode("x"))); +await check("malformed", "not.a.jwt"); +await check("alg", token, { algorithms: ["HS512"] }); + +// String durations: maxTokenAge and clockTolerance against the fixed date. +await check("max age 10 minutes", token, { maxTokenAge: "10 minutes" }); +await check("max age 1 hour", token, { maxTokenAge: "1 hour" }); +await check("max age 2 days", token, { maxTokenAge: "2 days" }); +await check("max age 1.5h", token, { maxTokenAge: "1.5h" }); +await check("max age +20m", token, { maxTokenAge: "+20m" }); +await check("max age 1 week", token, { maxTokenAge: "1 week" }); +await check("max age 1y", token, { maxTokenAge: "1y" }); +await check("max age 900 secs", token, { maxTokenAge: "900 secs" }); +await check("max age 3 HOURS", token, { maxTokenAge: "3 HOURS" }); +await check("tolerance 30s", token, { at: iat + 3620, clockTolerance: "30s" }); +await check("tolerance 5 sec", token, { at: iat + 3620, clockTolerance: "5 sec" }); +await check("tolerance 1 min from now", token, { at: iat + 3620, clockTolerance: "1 min from now" }); +await check("tolerance 1 min ago", token, { at: iat + 3620, clockTolerance: "1 min ago" }); +await check("bad duration", token, { maxTokenAge: "10 fortnights" }); +await check("signed ago", token, { maxTokenAge: "-2h ago" }); +await check("empty duration", token, { maxTokenAge: "" }); + +// String durations on the builder are relative to the clock: check the +// distance from now instead of printing it. +const relative = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt("-1 minute") + .setNotBefore("0s") + .setExpirationTime("2h") + .sign(secret); +const claims = decodeJwt(relative); +const clock = Math.floor(Date.now() / 1000); +console.log( + Math.abs((claims.exp ?? 0) - clock - 7200) <= 2, + Math.abs((claims.iat ?? 0) - clock + 60) <= 2, + Math.abs((claims.nbf ?? 0) - clock) <= 2, +); +const { payload } = await jwtVerify(relative, secret, { maxTokenAge: "5 minutes" }); +console.log(payload.role); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts new file mode 100644 index 000000000..730247dd1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts @@ -0,0 +1,55 @@ +export { compactDecrypt } from './jwe/compact/decrypt.js'; +export type { CompactDecryptGetKey } from './jwe/compact/decrypt.js'; +export { flattenedDecrypt } from './jwe/flattened/decrypt.js'; +export type { FlattenedDecryptGetKey } from './jwe/flattened/decrypt.js'; +export { generalDecrypt } from './jwe/general/decrypt.js'; +export type { GeneralDecryptGetKey } from './jwe/general/decrypt.js'; +export { GeneralEncrypt } from './jwe/general/encrypt.js'; +export type { Recipient } from './jwe/general/encrypt.js'; +export { compactVerify } from './jws/compact/verify.js'; +export type { CompactVerifyGetKey } from './jws/compact/verify.js'; +export { flattenedVerify } from './jws/flattened/verify.js'; +export type { FlattenedVerifyGetKey } from './jws/flattened/verify.js'; +export { generalVerify } from './jws/general/verify.js'; +export type { GeneralVerifyGetKey } from './jws/general/verify.js'; +export { jwtVerify } from './jwt/verify.js'; +export type { JWTVerifyOptions, JWTVerifyGetKey } from './jwt/verify.js'; +export { jwtDecrypt } from './jwt/decrypt.js'; +export type { JWTDecryptOptions, JWTDecryptGetKey } from './jwt/decrypt.js'; +export { CompactEncrypt } from './jwe/compact/encrypt.js'; +export { FlattenedEncrypt } from './jwe/flattened/encrypt.js'; +export { CompactSign } from './jws/compact/sign.js'; +export { FlattenedSign } from './jws/flattened/sign.js'; +export { GeneralSign } from './jws/general/sign.js'; +export type { Signature } from './jws/general/sign.js'; +export { SignJWT } from './jwt/sign.js'; +export { EncryptJWT } from './jwt/encrypt.js'; +export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js'; +export { EmbeddedJWK } from './jwk/embedded.js'; +export { createLocalJWKSet } from './jwks/local.js'; +export type { LocalJWKSet } from './jwks/local.js'; +export { createRemoteJWKSet, jwksCache, customFetch } from './jwks/remote.js'; +export type { RemoteJWKSet, RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache, FetchImplementation, } from './jwks/remote.js'; +export { UnsecuredJWT } from './jwt/unsecured.js'; +export type { UnsecuredResult } from './jwt/unsecured.js'; +export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js'; +export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js'; +export type { KeyImportOptions } from './key/import.js'; +export { decodeProtectedHeader } from './util/decode_protected_header.js'; +export { decodeJwt } from './util/decode_jwt.js'; +export type { ProtectedHeaderParameters } from './util/decode_protected_header.js'; +import * as errors from './util/errors.js'; +export { errors }; +export { generateKeyPair } from './key/generate_key_pair.js'; +export type { GenerateKeyPairAlgorithm, GenerateKeyPairResult, GenerateKeyPairOptions, } from './key/generate_key_pair.js'; +export { generateSecret } from './key/generate_secret.js'; +export type { GenerateSecretAlgorithm, GenerateSecretOptions } from './key/generate_secret.js'; +import * as base64url from './util/base64url.js'; +export { base64url }; +export type { AnyJWK, CompactDecryptResult, CompactJWEHeaderParameters, CompactJWSHeaderParameters, CompactVerifyResult, CritOption, CryptoKey, DecryptOptions, EncryptOptions, FlattenedDecryptResult, FlattenedJWE, FlattenedJWS, FlattenedJWSInput, FlattenedVerifyResult, GeneralDecryptResult, GeneralJWE, GeneralJWS, GeneralJWSInput, GeneralVerifyResult, GetKeyFunction, JoseHeaderParameters, JSONWebKeySet, JWEContentEncryptionAlgorithm, JWEHeaderParameters, JWEKeyManagementAlgorithm, JWEKeyManagementHeaderParameters, JWK_AKP_Private, JWK_AKP_Public, JWK_EC_Private, JWK_EC_Public, JWK_oct, JWK_OKP_Private, JWK_OKP_Public, JWK_RSA_Private, JWK_RSA_Public, JWK, JWKKeyType, JWKParameters, JWSAlgorithm, JWSHeaderParameters, JWTClaimVerificationOptions, JWTDecryptResult, JWTHeaderParameters, JWTPayload, JWTVerifyResult, KeyInput, KeyObject, ProduceJWT, ResolvedKey, SignOptions, VerifyOptions, } from './types.d.ts'; +/** + * Identified the loaded runtime-specific build in prior releases; now always `"WebCryptoAPI"`. + * + * @deprecated Remove any runtime branching on this value; it is always `"WebCryptoAPI"`. + */ +export declare const cryptoRuntime = "WebCryptoAPI"; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts new file mode 100644 index 000000000..abbac7ac2 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWE decryption from unverified headers and token data. */ +export interface CompactDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Compact JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, getKey: CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Compact JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput | CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts new file mode 100644 index 000000000..966f65c3f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts @@ -0,0 +1,40 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Compact JWE strings. */ +export declare class CompactEncrypt { + #private; + /** + * Creates a Compact JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Encrypts the plaintext as a Compact JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts new file mode 100644 index 000000000..b166f8f40 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWE decryption from unverified headers and token data. */ +export interface FlattenedDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Flattened JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Flattened JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, getKey: FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Flattened JWE with a key or key resolver. The result includes `key` only when a + * resolver is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput | FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts new file mode 100644 index 000000000..9d1d3e3fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts @@ -0,0 +1,46 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Flattened JWE objects. */ +export declare class FlattenedEncrypt { + #private; + /** + * Creates a Flattened JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Encrypts the plaintext as a Flattened JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts new file mode 100644 index 000000000..67a396c16 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts @@ -0,0 +1,28 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWE decryption from unverified headers and token data. */ +export interface GeneralDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a General JWE. + * + * > Note: Returns plaintext and headers from the first recipient that decrypts successfully. Other + * > recipients may be inspected to enforce serialization rules, but their headers are not included in + * > the result. Rely only on the returned data. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a General JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, getKey: GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a General JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput | GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts new file mode 100644 index 000000000..e738a3a14 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts @@ -0,0 +1,51 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual recipient in a General JWE. */ +export interface Recipient { + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): Recipient; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): Recipient; + /** + * Adds another recipient to the enclosing {@link GeneralEncrypt} and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** + * Encrypts for all recipients on the enclosing {@link GeneralEncrypt}, using their configured + * keys. + */ + encrypt(): Promise; + /** Returns the enclosing {@link GeneralEncrypt} instance. */ + done(): GeneralEncrypt; +} +/** Builds and encrypts General JWE objects. */ +export declare class GeneralEncrypt { + #private; + /** + * Creates a General JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Adds a recipient and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** Encrypts the plaintext as a General JWE. */ + encrypt(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts new file mode 100644 index 000000000..d02ffcd49 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** + * Resolves a verification key from an embedded "jwk" (JSON Web Key) Header Parameter. This key + * resolver opts JWS and JWT verification into trusting a public key supplied by the token. Use the + * verification function's `algorithms` option to restrict accepted algorithms. + * + * @param token The consumed JWS token. + * @returns The public key from the JWS "jwk" (JSON Web Key) Header Parameter. + */ +export declare function EmbeddedJWK(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts new file mode 100644 index 000000000..674d7cbe6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts @@ -0,0 +1,15 @@ +import type * as t from '../types.d.ts'; +/** + * Calculates a base64url-encoded JWK Thumbprint. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprint(key: t.JWK | t.CryptoKey | t.KeyObject, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; +/** + * Calculates a JWK Thumbprint URI. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprintUri(key: t.CryptoKey | t.KeyObject | t.JWK, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts new file mode 100644 index 000000000..57057c105 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts @@ -0,0 +1,17 @@ +import type * as t from '../types.d.ts'; +/** A key resolver created by {@link createLocalJWKSet}. */ +export interface LocalJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Returns a structured clone of the original JSON Web Key Set. */ + jwks: () => t.JSONWebKeySet; +} +/** + * Creates a resolver for a locally available JSON Web Key Set. Selection uses the header's "alg" + * (Algorithm) and "kid" (Key ID), and respects the JWK's "use" (Public Key Use) and "key_ops" (Key + * Operations). Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param jwks JSON Web Key Set formatted object. + */ +export declare function createLocalJWKSet(jwks: t.JSONWebKeySet): LocalJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts new file mode 100644 index 000000000..0b7055e84 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts @@ -0,0 +1,94 @@ +import type * as t from '../types.d.ts'; +/** + * Configures a custom fetch implementation for remote JWKS retrieval. + * + * > Note: Fetch-like libraries may have incompatible TypeScript signatures even when they accept the + * > supplied arguments at runtime. + */ +export declare const customFetch: unique symbol; +/** Custom fetch function. Must return HTTP 200 with a JSON JWKS. See {@link customFetch}. */ +export type FetchImplementation = ( +/** JWKS URL, passed as the fetch resource. */ +url: string, +/** Options passed to fetch. */ +options: { + headers: Headers; + method: 'GET'; + /** See {@link !Request.redirect} */ + redirect: 'manual'; + signal: AbortSignal; +}) => Promise; +/** + * Symbol used to configure an externally persisted remote JWKS cache. + * + * > Warning: Only trusted application code must be allowed to write this cache; its keys are trusted for + * > signature verification. + */ +export declare const jwksCache: unique symbol; +/** Remote JWKS resolver options. */ +export interface RemoteJWKSetOptions { + /** + * Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and + * the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds). + */ + timeoutDuration?: number; + /** + * Time in milliseconds after a successful fetch before a missing key can trigger another fetch. + * Must not be `NaN`. Defaults to 30000 (30 seconds). + */ + cooldownDuration?: number; + /** + * Maximum age of cached keys in milliseconds. Defaults to 600000 (10 minutes); `Infinity` + * disables expiry. Must not be `NaN`. + */ + cacheMaxAge?: number | typeof Infinity; + /** Headers to be sent with the HTTP request. */ + headers?: Record; + /** See {@link jwksCache}. */ + [jwksCache]?: JWKSCacheInput; + /** See {@link customFetch}. */ + [customFetch]?: FetchImplementation; +} +/** Shape of an externally persisted remote JWKS cache. */ +export interface ExportedJWKSCache { + /** Current cached JSON Web Key Set */ + jwks: t.JSONWebKeySet; + /** Last successful fetch, in milliseconds since Unix epoch. */ + uat: number; +} +/** Values accepted by the {@link jwksCache} option. */ +export type JWKSCacheInput = ExportedJWKSCache | Record; +/** A key resolver created by {@link createRemoteJWKSet}. */ +export interface RemoteJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Whether the cooldown window following the last successful fetch is still in effect. */ + readonly coolingDown: boolean; + /** + * Whether the currently cached JSON Web Key Set is within its + * {@link RemoteJWKSetOptions.cacheMaxAge}. + */ + readonly fresh: boolean; + /** Whether a JSON Web Key Set fetch is currently in flight. */ + readonly reloading: boolean; + /** + * Triggers a JSON Web Key Set fetch, bypassing + * {@link RemoteJWKSetOptions.cooldownDuration the cooldown}. + */ + reload: () => Promise; + /** + * Returns a structured clone of the cached JSON Web Key Set, or `undefined` before keys have been + * fetched or seeded via {@link jwksCache}. + */ + jwks: () => t.JSONWebKeySet | undefined; +} +/** + * Creates a resolver for a JSON Web Key Set available at an HTTP(S) URL. Fetches the JSON Web Key + * Set when the cache is missing or stale. An unmatched key triggers another fetch only when + * `cooldownDuration` has elapsed since the last successful fetch. Selection uses the header's "alg" + * and "kid" and respects the JWK's "use" and "key_ops". Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param url URL to fetch the JSON Web Key Set from. + */ +export declare function createRemoteJWKSet(url: URL, options?: RemoteJWKSetOptions): RemoteJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts new file mode 100644 index 000000000..15d7e062b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts @@ -0,0 +1,20 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Compact JWS strings. */ +export declare class CompactSign { + #private; + /** + * Creates a Compact JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWSHeaderParameters): this; + /** + * Signs the payload as a Compact JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts new file mode 100644 index 000000000..0f21744cb --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWS verification from unverified headers and token data. */ +export interface CompactVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Compact JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function compactVerify(jws: string | Uint8Array, getKey: CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Compact JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput | CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts new file mode 100644 index 000000000..3bc861803 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts @@ -0,0 +1,22 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Flattened JWS objects. */ +export declare class FlattenedSign { + #private; + /** + * Creates a Flattened JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): this; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): this; + /** + * Signs the payload as a Flattened JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts new file mode 100644 index 000000000..151c59b9a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWS verification from unverified headers and token data. */ +export interface FlattenedVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Flattened JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Flattened JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, getKey: FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Flattened JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput | FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts new file mode 100644 index 000000000..ad1cd73dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts @@ -0,0 +1,38 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual signature in a General JWS. */ +export interface Signature { + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): Signature; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): Signature; + /** + * Adds another signature to the enclosing {@link GeneralSign} and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Creates all signatures on the enclosing {@link GeneralSign}, using their configured keys. */ + sign(): Promise; + /** Returns the enclosing {@link GeneralSign} instance. */ + done(): GeneralSign; +} +/** Builds and signs General JWS objects. */ +export declare class GeneralSign { + #private; + /** + * Creates a General JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** + * Adds a signature and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Signs the payload as a General JWS. */ + sign(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts new file mode 100644 index 000000000..620325d00 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts @@ -0,0 +1,29 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWS verification from unverified headers and token data. */ +export interface GeneralVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a General JWS signature and decodes its payload. + * + * > Note: Returns payload and headers from the first signature that verifies successfully. Other entries' + * > headers may be inspected to reject inconsistent use of the JWS Unencoded Payload Option, but are + * > not included in the result. Rely only on the returned data. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a General JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, getKey: GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a General JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput | GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 000000000..cdf6a372a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts @@ -0,0 +1,34 @@ +import type * as t from '../types.d.ts'; +/** JWE decryption and JWT Claims Set validation options. */ +export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT decryption key. No token components have been authenticated when this function is + * called. + */ +export interface JWTDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE-formatted JWT and validates its Claims Set. + * + * @param key Private key or shared secret to decrypt and verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise>; +/** + * Decrypts a JWT and validates its claims, returning the dynamically + * {@link t.ResolvedKey.key resolved key}. + * + * @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT + * with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, getKey: JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Private key or shared secret, or a function resolving one, to decrypt and verify the + * JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 000000000..c39e4c994 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts @@ -0,0 +1,57 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */ +export declare class EncryptJWT extends EncryptJWT_base { + #private; + /** + * Sets the JWE Protected Header. May only be called once. + * + * @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc" + * (JWE Encryption Algorithm) properties. + */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds + * them to the appropriate JOSE Header. Use this instead of the header setters for algorithm + * inputs. May only be called once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Sets the content encryption key. By default, a suitable random key is generated for the JWE + * "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the Initialization Vector for content encryption. By default, a suitable random IV is + * generated for the JWE "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateIssuerAsHeader(): this; + /** + * Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateSubjectAsHeader(): this; + /** + * Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateAudienceAsHeader(): this; + /** + * Encrypts and returns the JWT. + * + * @param key Public key or shared secret to encrypt the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts new file mode 100644 index 000000000..6c3454eef --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** Decoded Unsecured JWT. */ +export interface UnsecuredResult { + /** JWT Claims Set. */ + payload: PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); + /** The decoded JOSE Header; always `{ "alg": "none" }` for an Unsecured JWT. */ + header: t.JWSHeaderParameters; +} +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const UnsecuredJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Encodes and decodes `{ "alg": "none" }` Unsecured JWTs. */ +export declare class UnsecuredJWT extends UnsecuredJWT_base { + private jwt; + /** Encodes the Unsecured JWT. */ + encode(): string; + /** Decodes an unsecured JWT and validates its claims without authenticating the token. */ + static decode(jwt: string, options?: t.JWTClaimVerificationOptions): UnsecuredResult; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts new file mode 100644 index 000000000..bbc7ef7d4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** + * Exports a public key to PEM-encoded SPKI. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportSPKI(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a private key to PEM-encoded PKCS#8. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportPKCS8(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a key to JWK. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportJWK(key: t.CryptoKey | t.KeyObject | Uint8Array): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts new file mode 100644 index 000000000..ecc53339c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts @@ -0,0 +1,33 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateKeyPair}, subject to runtime support. */ +export type GenerateKeyPairAlgorithm = 'PS256' | 'PS384' | 'PS512' | 'RS256' | 'RS384' | 'RS512' | 'RSA-OAEP' | 'RSA-OAEP-256' | 'RSA-OAEP-384' | 'RSA-OAEP-512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | (string & {}); +/** Generated asymmetric key pair. */ +export interface GenerateKeyPairResult { + privateKey: t.CryptoKey; + publicKey: t.CryptoKey; +} +/** Asymmetric key pair generation options. */ +export interface GenerateKeyPairOptions { + /** + * EC curve or OKP key subtype. Must be supported by the algorithm and runtime. ECDH-ES defaults + * to P-256. + */ + crv?: string; + /** RSA modulus length in bits. Must be an integer of at least 2048; defaults to 2048. */ + modulusLength?: number; + /** + * Whether the private key is extractable. Defaults to false; the public key is always + * extractable. + */ + extractable?: boolean; +} +/** + * Generates an asymmetric key pair for a JWA algorithm identifier. + * + * > Note: Private keys are not extractable by default. Set {@link GenerateKeyPairOptions.extractable} to + * > export them; public keys are always extractable. + * + * @param alg JWA Algorithm Identifier to be used with the generated key pair. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateKeyPair(alg: GenerateKeyPairAlgorithm, options?: GenerateKeyPairOptions): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts new file mode 100644 index 000000000..da1db236e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts @@ -0,0 +1,30 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateSecret}, subject to runtime support. */ +export type GenerateSecretAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW' | 'A128GCM' | 'A192GCM' | 'A256GCM' | (string & {}); +/** + * Maps a JWA algorithm identifier to the value returned by {@link generateSecret}. AES-CBC-HMAC + * algorithms return {@link !Uint8Array}; other supported algorithms return + * {@link t.CryptoKey CryptoKey}. When the algorithm is not statically known, the result is their + * union. + */ +export type GeneratedSecret = Alg extends 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' ? Uint8Array : string extends Alg ? t.CryptoKey | Uint8Array : t.CryptoKey; +/** Secret generation options. */ +export interface GenerateSecretOptions { + /** + * Whether the generated CryptoKey is extractable. Defaults to false; has no effect for + * A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512, which return raw bytes. + */ + extractable?: boolean; +} +/** + * Generates a symmetric secret key for a given JWA algorithm identifier. + * + * > Note: The secret key is generated with `extractable` set to `false` by default. + * + * > Note: A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512 return {@link !Uint8Array} because these secrets + * > have no CryptoKey representation. + * + * @param alg JWA Algorithm Identifier to be used with the generated secret. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateSecret(alg: Alg, options?: GenerateSecretOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts new file mode 100644 index 000000000..88ba06a28 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts @@ -0,0 +1,67 @@ +import type * as t from '../types.d.ts'; +/** + * Maps a JWK key type to the value returned by {@link importJWK}. An "oct" JWK returns + * {@link !Uint8Array}; other supported key types return {@link t.CryptoKey CryptoKey}. A JWK + * whose "kty" is not statically known resolves to their union. + */ +export type ImportedJWK = JWKType extends { + kty: 'oct'; +} ? Uint8Array : JWKType extends { + kty: 'AKP' | 'EC' | 'OKP' | 'RSA'; +} ? t.CryptoKey : t.CryptoKey | Uint8Array; +/** Key import options. */ +export interface KeyImportOptions { + /** + * Whether the imported CryptoKey is extractable. Overrides JWK "ext" when set. Without either, + * defaults to false for private keys, true otherwise. + */ + extractable?: boolean; +} +/** + * Imports a PEM-encoded SPKI string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param spki PEM-encoded SPKI string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importSPKI(spki: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded X.509 certificate's public key as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param x509 X.509 certificate string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importX509(x509: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded PKCS#8 string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param pkcs8 PEM-encoded PKCS#8 string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importPKCS8(pkcs8: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a JWK as a {@link !CryptoKey} or {@link !Uint8Array}. Asymmetric imports require either the + * "alg" argument or JWK "alg" parameter. For AKP keys, the JWK "alg" parameter is required and must + * match the argument when provided. + * + * > Note: CryptoKey imports honor the JWK "key_ops" and "ext" parameters. + * + * > Note: Symmetric JWKs (`kty: "oct"`) return {@link !Uint8Array} instead of CryptoKey. + * + * @param jwk JSON Web Key. + * @param alg JSON Web Algorithm identifier to be used with the imported key. Default is the "alg" + * property on the JWK. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importJWK(jwk: JWKType, alg?: string, options?: KeyImportOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts new file mode 100644 index 000000000..6d8a665fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts @@ -0,0 +1,16 @@ +/** + * Decodes base64url text or its UTF-8 bytes. + * + * @param input Base64url-encoded string or its UTF-8 bytes. + * @returns The decoded bytes. + * @throws {TypeError} If the input is not valid base64url. Standard Base64 `+` and `/` are + * rejected. + */ +export declare function decode(input: Uint8Array | string): Uint8Array; +/** + * Encodes unpadded base64url; strings are first encoded as UTF-8. + * + * @param input Bytes or a string to encode. Strings are first encoded as UTF-8. + * @returns The unpadded base64url representation of the input. + */ +export declare function encode(input: Uint8Array | string): string; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts new file mode 100644 index 000000000..63ce31f0e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts @@ -0,0 +1,8 @@ +import type * as t from '../types.d.ts'; +/** + * Decodes the Claims Set of a JWT in Compact JWS serialization without checking its signature or + * validating claim types and values. + * + * @returns The parsed JWT Claims Set. + */ +export declare function decodeJwt(jwt: string): PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts new file mode 100644 index 000000000..2d74643d3 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** JWE and JWS Header Parameters returned by {@link decodeProtectedHeader}. */ +export type ProtectedHeaderParameters = t.JWSHeaderParameters & t.JWEHeaderParameters; +/** + * Decodes the Protected Header of a JWE, JWS, or JWT without authenticating the token. + * + * @param token Compact token or JSON serialization object with a `protected` member. + * @returns The parsed Protected Header. + */ +export declare function decodeProtectedHeader(token: string | object): ProtectedHeaderParameters; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js new file mode 100644 index 000000000..a41eeb626 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js @@ -0,0 +1,70 @@ +import { compactDecrypt } from "./jwe/compact/decrypt.js"; +import { flattenedDecrypt } from "./jwe/flattened/decrypt.js"; +import { generalDecrypt } from "./jwe/general/decrypt.js"; +import { GeneralEncrypt } from "./jwe/general/encrypt.js"; +import { compactVerify } from "./jws/compact/verify.js"; +import { flattenedVerify } from "./jws/flattened/verify.js"; +import { generalVerify } from "./jws/general/verify.js"; +import { jwtVerify } from "./jwt/verify.js"; +import { jwtDecrypt } from "./jwt/decrypt.js"; +import { CompactEncrypt } from "./jwe/compact/encrypt.js"; +import { FlattenedEncrypt } from "./jwe/flattened/encrypt.js"; +import { CompactSign } from "./jws/compact/sign.js"; +import { FlattenedSign } from "./jws/flattened/sign.js"; +import { GeneralSign } from "./jws/general/sign.js"; +import { SignJWT } from "./jwt/sign.js"; +import { EncryptJWT } from "./jwt/encrypt.js"; +import { calculateJwkThumbprint, calculateJwkThumbprintUri } from "./jwk/thumbprint.js"; +import { EmbeddedJWK } from "./jwk/embedded.js"; +import { createLocalJWKSet } from "./jwks/local.js"; +import { createRemoteJWKSet, jwksCache, customFetch } from "./jwks/remote.js"; +import { UnsecuredJWT } from "./jwt/unsecured.js"; +import { exportPKCS8, exportSPKI, exportJWK } from "./key/export.js"; +import { importSPKI, importPKCS8, importX509, importJWK } from "./key/import.js"; +import { decodeProtectedHeader } from "./util/decode_protected_header.js"; +import { decodeJwt } from "./util/decode_jwt.js"; +import * as errors from "./util/errors.js"; +import { generateKeyPair } from "./key/generate_key_pair.js"; +import { generateSecret } from "./key/generate_secret.js"; +import * as base64url from "./util/base64url.js"; +const cryptoRuntime = "WebCryptoAPI"; +export { + CompactEncrypt, + CompactSign, + EmbeddedJWK, + EncryptJWT, + FlattenedEncrypt, + FlattenedSign, + GeneralEncrypt, + GeneralSign, + SignJWT, + UnsecuredJWT, + base64url, + calculateJwkThumbprint, + calculateJwkThumbprintUri, + compactDecrypt, + compactVerify, + createLocalJWKSet, + createRemoteJWKSet, + cryptoRuntime, + customFetch, + decodeJwt, + decodeProtectedHeader, + errors, + exportJWK, + exportPKCS8, + exportSPKI, + flattenedDecrypt, + flattenedVerify, + generalDecrypt, + generalVerify, + generateKeyPair, + generateSecret, + importJWK, + importPKCS8, + importSPKI, + importX509, + jwksCache, + jwtDecrypt, + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js new file mode 100644 index 000000000..3c20fe5df --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js @@ -0,0 +1,7 @@ +import { prepareDecrypt, decryptCompact } from "../../lib/jwe_decrypt.js"; +async function compactDecrypt(jwe, key, options) { + return decryptCompact(jwe, prepareDecrypt(options), key); +} +export { + compactDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js new file mode 100644 index 000000000..a500eccd6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js @@ -0,0 +1,26 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { compactJWE, createJWE } from "../../lib/jwe_encrypt.js"; +class CompactEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + async encrypt(key, options) { + return compactJWE(await createJWE([...this.#input], key, options)); + } +} +export { + CompactEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js new file mode 100644 index 000000000..cba368907 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js @@ -0,0 +1,15 @@ +import { JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { prepareDecrypt, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +async function flattenedDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("Flattened JWE must be an object"); + const shared = snapshotSharedJWE(jwe), [recipient, , error] = snapshotRecipientJWE(jwe); + if (!recipient) + throw error; + const snapshot = { ...shared, ...recipient }; + return checkRecipient(snapshot), decryptJWE(snapshot, prepareDecrypt(options), key); +} +export { + flattenedDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js new file mode 100644 index 000000000..7ee1b91ed --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js @@ -0,0 +1,35 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { createJWE } from "../../lib/jwe_encrypt.js"; +class FlattenedEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#input[3], "setSharedUnprotectedHeader"), this.#input[3] = sharedUnprotectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#input[4] = aad, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + async encrypt(key, options) { + return createJWE([...this.#input], key, options); + } +} +export { + FlattenedEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js new file mode 100644 index 000000000..dfdb66003 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js @@ -0,0 +1,40 @@ +import { prepareDecrypt, shareJWE, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { JWE, isJWECEKTransport } from "../../lib/jwe_algorithms.js"; +async function generalDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("General JWE must be an object"); + const inputRecipients = jwe.recipients; + if (!Array.isArray(inputRecipients)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + const recipients = Array.from(inputRecipients); + if (!recipients.every(isObject)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + if (!recipients.length) + throw new JWEInvalid("JWE Recipients has no members"); + let shared, sharedJwe, token; + try { + shared = prepareDecrypt(options), sharedJwe = snapshotSharedJWE(jwe), token = shareJWE(sharedJwe); + } catch { + throw new JWEDecryptionFailed(); + } + const recipientSnapshots = recipients.map((recipient) => snapshotRecipientJWE(recipient)); + if (recipients.length > 1) + for (const [, headerAlg] of recipientSnapshots) { + const alg = token[0]?.alg ?? headerAlg ?? sharedJwe.unprotected?.alg, algEntry = typeof alg == "string" ? JWE[alg] : void 0; + if (algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + } + for (const [recipient] of recipientSnapshots) + if (recipient) + try { + const flattened = { ...sharedJwe, ...recipient }; + return checkRecipient(flattened), await decryptJWE(flattened, shared, key, token); + } catch { + } + throw new JWEDecryptionFailed(); +} +export { + generalDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js new file mode 100644 index 000000000..9fe068a8a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js @@ -0,0 +1,105 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { JWEInvalid } from "../../util/errors.js"; +import { generateCek } from "../../lib/content_encryption.js"; +import { encode as b64u } from "../../util/base64url.js"; +import { checkDisjoint, checkEncryptHeaders, encryptJWE } from "../../lib/jwe_encrypt.js"; +import { encryptKeyManagement } from "../../lib/key_management.js"; +import { isJWECEKTransport, jweAlgorithm } from "../../lib/jwe_algorithms.js"; +class IndividualRecipient { + #parent; + state; + constructor(enc, key, crit) { + this.#parent = enc, this.state = [void 0, void 0, key, crit]; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[0], "setUnprotectedHeader"), this.state[0] = unprotectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.state[1], "setKeyManagementParameters"), this.state[1] = parameters, this; + } + addRecipient(...args) { + return this.#parent.addRecipient(...args); + } + encrypt(...args) { + return this.#parent.encrypt(...args); + } + done() { + return this.#parent; + } +} +class GeneralEncrypt { + #plaintext; + #recipients = []; + #protectedHeader; + #unprotectedHeader; + #aad; + constructor(plaintext) { + this.#plaintext = plaintext; + } + addRecipient(key, options) { + const recipient = new IndividualRecipient(this, key, options?.crit); + return this.#recipients.push(recipient), recipient; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#unprotectedHeader, "setSharedUnprotectedHeader"), this.#unprotectedHeader = sharedUnprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#aad = aad, this; + } + async encrypt() { + if (!this.#recipients.length) + throw new JWEInvalid("at least one recipient must be added"); + assertUint8Array(this.#plaintext, "plaintext"); + const multiple = this.#recipients.length > 1; + let enc, protectedHeader = this.#protectedHeader, sharedUnprotectedHeader = this.#unprotectedHeader; + const recipients = []; + for (const recipient of this.#recipients) { + const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state, input = [ + this.#plaintext, + protectedHeader, + unprotectedHeader, + sharedUnprotectedHeader, + this.#aad, + void 0, + void 0, + keyManagementParameters, + crit, + multiple + ], headers = checkEncryptHeaders(input, void 0, recipients.length > 0); + recipients.length || (protectedHeader = input[1], sharedUnprotectedHeader = input[3]), recipients.push([input, headers, key]); + const [{ alg, enc: recipientEnc }, , algEntry] = headers; + if (multiple && algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + if (!enc) + enc = recipientEnc; + else if (enc !== recipientEnc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); + } + for (const [, headers] of recipients) + headers[2] ??= jweAlgorithm(headers[0].alg); + const [firstInput, firstHeaders, firstKey] = recipients[0], cek = multiple ? generateCek(firstHeaders[1]) : void 0; + firstInput[5] = cek; + const { encrypted_key, header, ...shared } = await encryptJWE(firstInput, firstHeaders, firstKey), jwe = { + ...shared, + recipients: [{}] + }; + encrypted_key && (jwe.recipients[0].encrypted_key = encrypted_key), header && (jwe.recipients[0].header = header); + for (let i = 1; i < recipients.length; i++) { + const [input, [joseHeader, encEntry, algEntry], key] = recipients[i], unprotectedHeader = input[2], [, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, cek, input[7]), target = { + encrypted_key: b64u(encryptedKey) + }; + if (unprotectedHeader || parameters) { + const header2 = { ...unprotectedHeader, ...parameters }; + parameters && checkDisjoint(input[1], header2, input[3]), target.header = header2; + } + jwe.recipients.push(target); + } + return jwe; + } +} +export { + GeneralEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js new file mode 100644 index 000000000..1e1dd89c4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js @@ -0,0 +1,30 @@ +import { jwkToKey, normalizeJwk } from "../lib/key.js"; +import { jwsAlgorithm } from "../lib/jws_algorithms.js"; +import { isObject } from "../lib/validate.js"; +import { JWSInvalid } from "../util/errors.js"; +async function EmbeddedJWK(protectedHeader, token) { + const joseHeader = { + ...protectedHeader, + ...token?.header + }; + if (!isObject(joseHeader.jwk)) + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object'); + let jwk; + try { + jwk = normalizeJwk(joseHeader.jwk); + } catch (cause) { + throw new JWSInvalid("Invalid Embedded JWK", { cause }); + } + const entry = jwsAlgorithm(joseHeader.alg); + if (jwk.use !== void 0 && jwk.use !== "sig") + throw new JWSInvalid('Invalid Embedded JWK, its "use" must be "sig" when present'); + if (jwk.alg !== void 0 && jwk.alg !== entry.alg) + throw new JWSInvalid(`Invalid Embedded JWK, its "alg" must be "${entry.alg}" when present`); + const key = await jwkToKey(entry, jwk, !0); + if (key.type !== "public") + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key'); + return key; +} +export { + EmbeddedJWK +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js new file mode 100644 index 000000000..ec93e222e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js @@ -0,0 +1,55 @@ +import { digest, encode } from "../lib/buffer_utils.js"; +import { encode as b64u } from "../util/base64url.js"; +import { JOSENotSupported, JWKInvalid } from "../util/errors.js"; +import { isKeyLike, invalidKeyInput, snapshotJwk } from "../lib/key.js"; +import { isObject } from "../lib/validate.js"; +import { exportJWK } from "../key/export.js"; +const check = (value, description) => { + if (typeof value != "string" || !value) + throw new JWKInvalid(`${description} missing or invalid`); +}; +async function calculateJwkThumbprint(key, digestAlgorithm) { + let jwk; + if (isObject(key)) { + if (jwk = snapshotJwk(key), typeof jwk.kty != "string") + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + } else if (isKeyLike(key)) + jwk = snapshotJwk(await exportJWK(key)); + else + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + if (digestAlgorithm ??= "sha256", digestAlgorithm !== "sha256" && digestAlgorithm !== "sha384" && digestAlgorithm !== "sha512") + throw new TypeError('digestAlgorithm must one of "sha256", "sha384", or "sha512"'); + let components; + switch (jwk.kty) { + case "AKP": + check(jwk.alg, '"alg" (Algorithm) Parameter'), check(jwk.pub, '"pub" (Public key) Parameter'), components = { alg: jwk.alg, kty: jwk.kty, pub: jwk.pub }; + break; + case "EC": + check(jwk.crv, '"crv" (Curve) Parameter'), check(jwk.x, '"x" (X Coordinate) Parameter'), check(jwk.y, '"y" (Y Coordinate) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + break; + case "OKP": + check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter'), check(jwk.x, '"x" (Public Key) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; + break; + case "RSA": + check(jwk.e, '"e" (Exponent) Parameter'), check(jwk.n, '"n" (Modulus) Parameter'), components = { e: jwk.e, kty: jwk.kty, n: jwk.n }; + break; + case "oct": + if (typeof jwk.k != "string") + throw new JWKInvalid('"k" (Key Value) Parameter missing or invalid'); + components = { k: jwk.k, kty: jwk.kty }; + break; + default: + throw new JOSENotSupported('"kty" (Key Type) Parameter missing or unsupported'); + } + const data = encode(JSON.stringify(components)); + return b64u(await digest(digestAlgorithm, data)); +} +async function calculateJwkThumbprintUri(key, digestAlgorithm) { + digestAlgorithm ??= "sha256"; + const thumbprint = await calculateJwkThumbprint(key, digestAlgorithm); + return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`; +} +export { + calculateJwkThumbprint, + calculateJwkThumbprintUri +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js new file mode 100644 index 000000000..448757265 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js @@ -0,0 +1,59 @@ +import { jwkToKey, snapshotJwk } from "../lib/key.js"; +import { JWS } from "../lib/jws_algorithms.js"; +import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys } from "../util/errors.js"; +import { isJwkSet } from "../lib/validate.js"; +function isUsableJWK(jwk, entry, alg, kid) { + const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk; + return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv); +} +async function importWithAlgCache(cache, jwk, entry) { + const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk), { alg } = entry; + if (cached[alg] === void 0) { + const pending = jwkToKey(entry, jwk, !0).then((key) => { + if (key.type !== "public") + throw new JWKSInvalid("JSON Web Key Set members must be public keys"); + return cached[alg] = key, key; + }).catch((error) => { + throw cached[alg] === pending && delete cached[alg], error; + }); + cached[alg] = pending; + } + return cached[alg]; +} +function createLocalJWKSet(jwks) { + let snapshot; + try { + snapshot = structuredClone(jwks); + } catch { + } + if (!isJwkSet(snapshot)) + throw new JWKSInvalid("JSON Web Key Set malformed"); + const metadata = snapshot.keys.map((jwk) => { + const normalized = snapshotJwk(jwk); + return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized; + }), cached = /* @__PURE__ */ new WeakMap(); + return Object.defineProperty(async (protectedHeader, token) => { + const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry || entry.secret) + throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); + const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates; + if (!length) + throw new JWKSNoMatchingKey(); + if (length !== 1) { + const error = new JWKSMultipleMatchingKeys(); + throw error[Symbol.asyncIterator] = async function* () { + for (const jwk2 of candidates) + try { + yield await importWithAlgCache(cached, jwk2, entry); + } catch { + } + }, error; + } + return importWithAlgCache(cached, jwk, entry); + }, "jwks", { + value: () => structuredClone(snapshot) + }); +} +export { + createLocalJWKSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js new file mode 100644 index 000000000..39519f27b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js @@ -0,0 +1,101 @@ +import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js"; +import { createLocalJWKSet } from "./local.js"; +import { isJwkSet } from "../lib/validate.js"; +function isCloudflareWorkers() { + return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel"; +} +let USER_AGENT; +(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12"); +const customFetch = /* @__PURE__ */ Symbol(); +async function fetchJwks(url, headers, signal, fetchImpl = fetch) { + const response = await fetchImpl(url, { + method: "GET", + signal, + redirect: "manual", + headers + }).catch((err) => { + throw err.name === "TimeoutError" ? new JWKSTimeout() : err; + }); + if (response.status !== 200) + throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); + try { + return await response.json(); + } catch { + throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); + } +} +const jwksCache = /* @__PURE__ */ Symbol(); +function isFreshFor(timestamp, duration) { + return Number.isFinite(timestamp) && Date.now() < timestamp + duration; +} +function validateDuration(value, fallback, option) { + if (Number.isNaN(value)) + throw new TypeError(`"${option}" option must not be NaN`); + return typeof value == "number" ? value : fallback; +} +function createRemoteJWKSet(url, options) { + if (!(url instanceof URL)) + throw new TypeError("url must be an instance of URL"); + const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration; + if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) + throw new TypeError('"timeoutDuration" option must be a non-negative integer'); + const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers); + USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json"); + const fetchImpl = opts[customFetch], cache = opts[jwksCache]; + let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local; + if (cache && typeof cache == "object") { + const { uat, jwks } = cache; + isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks)); + } + const reload = async () => { + if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) { + const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => { + const next = createLocalJWKSet(json); + if (sequence <= appliedSequence) + return; + local = next; + const updatedAt = Date.now(); + cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence; + }).finally(() => { + pendingFetch === current && (pendingFetch = void 0); + }); + } + await pendingFetch; + }; + return Object.defineProperties(async (protectedHeader, token) => { + (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload(); + try { + return await local(protectedHeader, token); + } catch (err) { + if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) + return await reload(), local(protectedHeader, token); + throw err; + } + }, { + coolingDown: { + get: () => isFreshFor(jwksTimestamp, cooldownDuration), + enumerable: !0 + }, + fresh: { + get: () => isFreshFor(jwksTimestamp, cacheMaxAge), + enumerable: !0 + }, + reload: { + value: reload, + enumerable: !0 + }, + reloading: { + get: () => !!pendingFetch, + enumerable: !0 + }, + jwks: { + value: () => local?.jwks(), + enumerable: !0 + } + }); +} +export { + createRemoteJWKSet, + customFetch, + jwksCache +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js new file mode 100644 index 000000000..f3288d2f0 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js @@ -0,0 +1,20 @@ +import { createCompactSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class CompactSign { + #payload; + #protectedHeader; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#payload = payload; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(this.#payload, this.#protectedHeader, options?.crit, key, () => { + throw new TypeError("use the flattened module for creating JWS with b64: false"); + }); + } +} +export { + CompactSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js new file mode 100644 index 000000000..49625ee85 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js @@ -0,0 +1,8 @@ +import { prepareVerify, verifyCompact } from "../../lib/jws_verify.js"; +async function compactVerify(jws, key, options) { + const [result] = await verifyCompact(jws, prepareVerify(options), key); + return result; +} +export { + compactVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js new file mode 100644 index 000000000..7e321402f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js @@ -0,0 +1,23 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class FlattenedSign { + #input; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#input = [payload]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + async sign(key, options) { + const input = [...this.#input]; + input[3] = options?.crit; + const [jws] = await createSignature(input, key); + return jws; + } +} +export { + FlattenedSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js new file mode 100644 index 000000000..f4a7c9541 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js @@ -0,0 +1,12 @@ +import { JWSInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { encodeJsonUnencodedPayload, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +async function flattenedVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("Flattened JWS must be an object"); + const snapshot = snapshotJws(jws), [result] = await verifySignature(snapshot, prepareVerify(options), key, encodeJsonUnencodedPayload); + return result; +} +export { + flattenedVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js new file mode 100644 index 000000000..8f76dd00b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js @@ -0,0 +1,58 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { JWSInvalid } from "../../util/errors.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class IndividualSignature { + #parent; + state; + constructor(sig, key, options) { + this.#parent = sig, this.state = [void 0, void 0, key, options?.crit]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.state[0], "setProtectedHeader"), this.state[0] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[1], "setUnprotectedHeader"), this.state[1] = unprotectedHeader, this; + } + addSignature(...args) { + return this.#parent.addSignature(...args); + } + sign(...args) { + return this.#parent.sign(...args); + } + done() { + return this.#parent; + } +} +class GeneralSign { + #payload; + #signatures = []; + constructor(payload) { + this.#payload = payload; + } + addSignature(key, options) { + const signature = new IndividualSignature(this, key, options); + return this.#signatures.push(signature), signature; + } + async sign() { + if (!this.#signatures.length) + throw new JWSInvalid("at least one signature must be added"); + assertUint8Array(this.#payload, "payload"); + const jws = { + signatures: [], + payload: "" + }, encoded = []; + let b64; + for (const signature of this.#signatures) { + const [protectedHeader, unprotectedHeader, key, crit] = signature.state, [{ payload, ...rest }, signatureB64] = await createSignature([this.#payload, protectedHeader, unprotectedHeader, crit, encoded], key); + if (b64 === void 0) + b64 = signatureB64, jws.payload = payload; + else if (b64 !== signatureB64) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + jws.signatures.push(rest); + } + return jws; + } +} +export { + GeneralSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js new file mode 100644 index 000000000..3243408b8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js @@ -0,0 +1,48 @@ +import { encodeJsonUnencodedPayload, parseProtectedHeader, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +import { JWSInvalid, JWSSignatureVerificationFailed } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +function snapshotSignature(signature, payload) { + try { + const jws = snapshotJws(signature, [payload]), protectedHeader = parseProtectedHeader(jws.protected), { b64, crit } = protectedHeader; + return [ + jws, + protectedHeader, + Array.isArray(crit) && crit.includes("b64") ? typeof b64 == "boolean" ? b64 ? 1 : 2 : 0 : 1 + ]; + } catch { + return; + } +} +async function generalVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("General JWS must be an object"); + const { signatures, payload: inputPayload } = jws; + if (!Array.isArray(signatures)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + const signatureEntries = Array.from(signatures); + if (!signatureEntries.every(isObject)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + let shared; + try { + if (inputPayload === void 0) + throw new Error(); + shared = prepareVerify(options); + } catch { + throw new JWSSignatureVerificationFailed(); + } + const payload = inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload, candidates = signatureEntries.map((signature) => snapshotSignature(signature, payload)).filter((candidate) => candidate !== void 0); + let modes = 0; + for (const [, , mode] of candidates) + if (modes |= mode, modes === 3) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + for (const candidate of candidates) + try { + const [result] = await verifySignature(candidate[0], shared, key, encodeJsonUnencodedPayload, candidate[1]); + return result; + } catch { + } + throw new JWSSignatureVerificationFailed(); +} +export { + generalVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 000000000..cfbe4730c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js @@ -0,0 +1,13 @@ +import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTClaimValidationFailed } from "../util/errors.js"; +async function jwtDecrypt(jwt, key, options) { + const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options); + for (const claim of ["iss", "sub", "aud"]) + if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim])) + throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch"); + return { payload, ...result }; +} +export { + jwtDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 000000000..c26908857 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js @@ -0,0 +1,45 @@ +import { compactJWE, createJWE } from "../lib/jwe_encrypt.js"; +import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const EncryptJWT_base = JWTClaimsBuilder; +class EncryptJWT extends EncryptJWT_base { + #input = [void 0]; + #replicateIssuerAsHeader; + #replicateSubjectAsHeader; + #replicateAudienceAsHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + replicateIssuerAsHeader() { + return this.#replicateIssuerAsHeader = !0, this; + } + replicateSubjectAsHeader() { + return this.#replicateSubjectAsHeader = !0, this; + } + replicateAudienceAsHeader() { + return this.#replicateAudienceAsHeader = !0, this; + } + async encrypt(key, options) { + const plaintext = jwtData(this); + this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = { + ...this.#input[1], + iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0, + sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0, + aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0 + }); + const input = [...this.#input]; + return input[0] = plaintext, compactJWE(await createJWE(input, key, options)); + } +} +export { + EncryptJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js new file mode 100644 index 000000000..0f01cc7a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js @@ -0,0 +1,34 @@ +import * as b64u from "../util/base64url.js"; +import { decodeBase64url, parseJoseHeader, JWS_RECOGNIZED, validateB64, validateCrit } from "../lib/validate.js"; +import { JWSInvalid, JWTInvalid } from "../util/errors.js"; +import { validateClaimsSet, JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +const UnsecuredJWT_base = JWTClaimsBuilder; +class UnsecuredJWT extends UnsecuredJWT_base { + encode() { + const header = b64u.encode(JSON.stringify({ alg: "none" })), payload = b64u.encode(jwtData(this)); + return `${header}.${payload}.`; + } + static decode(jwt, options) { + if (typeof jwt != "string") + throw new JWTInvalid("Unsecured JWT must be a string"); + const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split("."); + if (length !== 3 || signature !== "") + throw new JWTInvalid("Invalid Unsecured JWT"); + let header, b64; + try { + header = parseJoseHeader(encodedHeader, JWSInvalid, "JWS Protected Header is invalid"); + const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, void 0, header, header); + b64 = validateB64(header, extensions); + } catch (cause) { + throw cause instanceof JWSInvalid ? new JWTInvalid("Invalid Unsecured JWT", { cause }) : cause; + } + if (header.alg !== "none") + throw new JWTInvalid("Invalid Unsecured JWT"); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + return { payload: validateClaimsSet(header, decodeBase64url(encodedPayload, "payload", JWTInvalid), options), header }; + } +} +export { + UnsecuredJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js new file mode 100644 index 000000000..49016c5a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js @@ -0,0 +1,35 @@ +import { toSPKI as exportPublic, toPKCS8 as exportPrivate } from "../lib/asn1.js"; +import { invalidKeyInput, isCryptoKey, isKeyObject } from "../lib/key.js"; +import { encode as b64u } from "../util/base64url.js"; +function exportSPKI(key) { + return exportPublic(key); +} +function exportPKCS8(key) { + return exportPrivate(key); +} +async function exportJWK(key) { + if (isKeyObject(key)) + if (key.type === "secret") + key = key.export(); + else + return key.export({ format: "jwk" }); + if (key instanceof Uint8Array) + return { + kty: "oct", + k: b64u(key) + }; + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "Uint8Array")); + if (!key.extractable) + throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK"); + const jwk = await crypto.subtle.exportKey("jwk", key); + delete jwk.ext, delete jwk.key_ops, delete jwk.use, jwk.kty !== "AKP" && delete jwk.alg; + for (const parameter of Object.keys(jwk)) + jwk[parameter] === void 0 && delete jwk[parameter]; + return jwk; +} +export { + exportJWK, + exportPKCS8, + exportSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js new file mode 100644 index 000000000..c687f420d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js @@ -0,0 +1,35 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { validateExtractableOption } from "../lib/key.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +function getModulusLengthOption(options) { + const modulusLength = options?.modulusLength ?? 2048; + if (typeof modulusLength != "number" || !Number.isInteger(modulusLength) || modulusLength < 2048) + throw new JOSENotSupported("Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used"); + return modulusLength; +} +async function generateKeyPair(alg, options) { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + let algorithm; + if (entry.resolve) { + const crv = options?.crv ?? "P-256"; + if (!["P-256", "P-384", "P-521", "X25519"].includes(crv)) + throw new JOSENotSupported("Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519"); + algorithm = entry.resolve({ crv }); + } else { + if (entry.crv !== void 0 && options?.crv !== void 0 && options.crv !== entry.crv) + throw new JOSENotSupported(`Invalid or unsupported crv option provided, the only supported value for ${alg} is ${entry.crv}`); + algorithm = entry.kty[0] === "RSA" ? { + ...entry.subtle, + publicExponent: Uint8Array.of(1, 0, 1), + modulusLength: getModulusLengthOption(options) + } : entry.subtle; + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, [ + ...entry.usages[1], + ...entry.usages[0] + ]); +} +export { + generateKeyPair +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js new file mode 100644 index 000000000..b085b4919 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js @@ -0,0 +1,36 @@ +import { unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +import { validateExtractableOption } from "../lib/key.js"; +async function generateSecret(alg, options) { + const extractable = validateExtractableOption(options?.extractable); + let length, algorithm, keyUsages; + switch (alg) { + case "HS256": + case "HS384": + case "HS512": + length = +alg.slice(-3), algorithm = { name: "HMAC", hash: `SHA-${length}`, length }, keyUsages = ["sign", "verify"]; + break; + case "A128CBC-HS256": + case "A192CBC-HS384": + case "A256CBC-HS512": + return crypto.getRandomValues(new Uint8Array(+alg.slice(-3) >> 3)); + case "A128KW": + case "A192KW": + case "A256KW": + length = +alg.slice(1, 4), algorithm = { name: "AES-KW", length }, keyUsages = ["wrapKey", "unwrapKey"]; + break; + case "A128GCMKW": + case "A192GCMKW": + case "A256GCMKW": + case "A128GCM": + case "A192GCM": + case "A256GCM": + length = +alg.slice(1, 4), algorithm = { name: "AES-GCM", length }, keyUsages = ["encrypt", "decrypt"]; + break; + default: + unsupportedAlg(algArgument); + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, keyUsages); +} +export { + generateSecret +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js new file mode 100644 index 000000000..b171ae5de --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js @@ -0,0 +1,53 @@ +import { decode as decodeBase64URL } from "../util/base64url.js"; +import { fromSPKI, fromPKCS8, fromX509 } from "../lib/asn1.js"; +import { jwkToKey, validateExtractableOption, normalizeJwk } from "../lib/key.js"; +import { keyAlgorithm } from "../lib/key_algorithm.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { isObject } from "../lib/validate.js"; +async function importSPKI(spki, alg, options) { + if (typeof spki != "string" || spki.indexOf("-----BEGIN PUBLIC KEY-----") !== 0) + throw new TypeError('"spki" must be SPKI formatted string'); + return fromSPKI(spki, alg, options); +} +async function importX509(x509, alg, options) { + if (typeof x509 != "string" || x509.indexOf("-----BEGIN CERTIFICATE-----") !== 0) + throw new TypeError('"x509" must be X.509 formatted string'); + return fromX509(x509, alg, options); +} +async function importPKCS8(pkcs8, alg, options) { + if (typeof pkcs8 != "string" || pkcs8.indexOf("-----BEGIN PRIVATE KEY-----") !== 0) + throw new TypeError('"pkcs8" must be PKCS#8 formatted string'); + return fromPKCS8(pkcs8, alg, options); +} +async function importJWK(jwk, alg, options) { + if (!isObject(jwk)) + throw new TypeError("JWK must be an object"); + const normalized = normalizeJwk(jwk), extractable = validateExtractableOption(options?.extractable), { alg: jwkAlg } = normalized; + if (alg ??= jwkAlg, normalized.kty !== "oct" && !alg) + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + switch (normalized.kty) { + case "oct": + if (typeof normalized.k != "string") + throw new TypeError('missing "k" (Key Value) Parameter value'); + return decodeBase64URL(normalized.k); + case "AKP": { + if (typeof jwkAlg != "string" || !jwkAlg) + throw new TypeError('missing "alg" (Algorithm) Parameter value'); + if (alg !== jwkAlg) + throw new TypeError("JWK alg and alg option value mismatch"); + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + } + case "RSA": + case "EC": + case "OKP": + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } +} +export { + importJWK, + importPKCS8, + importSPKI, + importX509 +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js new file mode 100644 index 000000000..51f781b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js @@ -0,0 +1,138 @@ +import { invalidKeyInput, isCryptoKey, isKeyObject, validateExtractableOption } from "./key.js"; +import { encodeBase64, decodeBase64 } from "./buffer_utils.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "./key_algorithm.js"; +const formatPEM = (b64, descriptor) => { + const newlined = (b64.match(/.{1,64}/g) || []).join(` +`); + return `-----BEGIN ${descriptor}----- +${newlined} +-----END ${descriptor}-----`; +}, genericExport = async (keyType, keyFormat, key) => { + if (isKeyObject(key)) { + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return key.export({ format: "pem", type: keyFormat }); + } + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject")); + if (!key.extractable) + throw new TypeError("CryptoKey is not extractable"); + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`); +}, toSPKI = (key) => genericExport("public", "spki", key), toPKCS8 = (key) => genericExport("private", "pkcs8", key), bytesEqual = (a, b) => { + if (a.byteLength !== b.length) + return !1; + for (let i = 0; i < a.byteLength; i++) + if (a[i] !== b[i]) + return !1; + return !0; +}, createASN1State = (data) => ({ data, pos: 0 }), readByte = (state) => { + const byte = state.data[state.pos++]; + if (byte === void 0) + throw new Error("Unexpected end of ASN.1 input"); + return byte; +}, parseLength = (state) => { + const first = readByte(state); + if (first & 128) { + const lengthOfLen = first & 127; + let length = 0; + for (let i = 0; i < lengthOfLen; i++) + length = length << 8 | readByte(state); + return length; + } + return first; +}, skipElement = (state, count = 1) => { + for (; count-- > 0; ) { + state.pos++; + const length = parseLength(state); + state.pos += length; + } +}, expectTag = (state, expectedTag, errorMessage) => { + if (readByte(state) !== expectedTag) + throw new Error(errorMessage); +}, getSubarray = (state, length) => { + if (length < 0 || state.pos + length > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + const result = state.data.subarray(state.pos, state.pos + length); + return state.pos += length, result; +}, parseAlgorithmOID = (state) => { + expectTag(state, 6, "Expected algorithm OID"); + const oidLen = parseLength(state); + return getSubarray(state, oidLen); +}; +function parseKeyHeader(state, keyFormat) { + if (expectTag(state, 48, `Invalid ${keyFormat === "spki" ? "SPKI" : "PKCS#8"} structure`), parseLength(state), keyFormat === "pkcs8") { + expectTag(state, 2, "Expected version field"); + const length = parseLength(state); + state.pos += length; + } + expectTag(state, 48, "Expected algorithm identifier"), parseLength(state); +} +const parseECAlgorithmIdentifier = (state) => { + const algOid = parseAlgorithmOID(state); + if (bytesEqual(algOid, [43, 101, 110])) + return "X25519"; + if (!bytesEqual(algOid, [42, 134, 72, 206, 61, 2, 1])) + throw new Error("Unsupported key algorithm"); + expectTag(state, 6, "Expected curve OID"); + const curveOidLen = parseLength(state), curveOid = getSubarray(state, curveOidLen); + if (bytesEqual(curveOid, [42, 134, 72, 206, 61, 3, 1, 7])) + return "P-256"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 34])) + return "P-384"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 35])) + return "P-521"; + throw new Error("Unsupported named curve"); +}, genericImport = async (keyFormat, keyData, alg, options) => { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + const isPublic = keyFormat === "spki"; + let algorithm; + if (entry.resolve) + try { + const state = createASN1State(keyData); + parseKeyHeader(state, keyFormat), algorithm = entry.resolve({ crv: parseECAlgorithmIdentifier(state) }); + } catch { + throw new JOSENotSupported("Invalid or unsupported key format"); + } + else + algorithm = entry.subtle; + return crypto.subtle.importKey(keyFormat, keyData, algorithm, extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); +}, processPEMData = (pem, pattern) => decodeBase64(pem.replace(pattern, "")), fromPKCS8 = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g); + return genericImport("pkcs8", keyData, alg, options); +}, fromSPKI = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g); + return genericImport("spki", keyData, alg, options); +}; +function spkiFromX509(buf) { + const state = createASN1State(buf); + expectTag(state, 48, "Invalid certificate structure"); + const certificateLength = parseLength(state); + if (certificateLength < 0 || state.pos + certificateLength > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + expectTag(state, 48, "Invalid tbsCertificate structure"), parseLength(state), buf[state.pos] === 160 ? skipElement(state, 6) : skipElement(state, 5); + const spkiStart = state.pos; + expectTag(state, 48, "Invalid SPKI structure"); + const spkiContentLen = parseLength(state); + return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart)); +} +const fromX509 = (pem, alg, options) => { + let spki; + try { + const certificate = processPEMData(pem, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); + spki = spkiFromX509(certificate); + } catch (cause) { + throw new TypeError("Failed to parse the X.509 certificate", { cause }); + } + return genericImport("spki", spki, alg, options); +}; +export { + fromPKCS8, + fromSPKI, + fromX509, + toPKCS8, + toSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 000000000..1ea355e91 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js @@ -0,0 +1,90 @@ +import { concat, uint64be } from "./buffer_utils.js"; +import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js"; +const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3)); +function checkCekLength(cek, expected) { + const actual = cek.byteLength << 3; + if (actual !== expected) + throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`); +} +const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3)); +function checkIvLength(enc, iv) { + if (iv.length << 3 !== enc.ivBits) + throw new JWEInvalid("Invalid Initialization Vector length"); +} +async function cbcKeySetup(enc, cek, usage) { + if (!(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "Uint8Array")); + const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: "HMAC" + }, !1, ["sign"]); + return [encKey, macKey, keySize]; +} +async function cbcHmacTag(macKey, macData, keySize) { + return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3)); +} +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: "AES-CBC" + }, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize); + return { ciphertext, tag, iv }; +} +async function timingSafeEqual(a, b) { + const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a); + return crypto.subtle.verify(algorithm, key, aHmac, b); +} +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize); + try { + if (await timingSafeEqual(tag, expectedTag)) + return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext)); + } catch { + } + throw new JWEDecryptionFailed(); +} +async function encrypt(enc, plaintext, cek, iv, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcEncrypt(enc, plaintext, cek, iv, aad); + const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, plaintext)); + return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv }; +} +async function decrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (!iv) + throw new JWEInvalid("JWE Initialization Vector missing"); + if (!tag) + throw new JWEInvalid("JWE Authentication Tag missing"); + if (!enc.cbc && tag.length !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + const encKey = await rawKey(cek, enc.subtle, "decrypt"); + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, concat(ciphertext, tag))); + } catch { + throw new JWEDecryptionFailed(); + } +} +export { + checkCekLength, + checkIvLength, + decrypt, + encrypt, + generateCek, + generateIv +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 000000000..27ffba278 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js @@ -0,0 +1,40 @@ +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { concat } from "./buffer_utils.js"; +function validateZip(joseHeader, protectedHeader) { + if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") + throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); + if (joseHeader.zip !== void 0 && !protectedHeader?.zip) + throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); +} +function supported(name) { + if (typeof globalThis[name] > "u") + throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`); +} +async function transform(stream, input, maxLength = 1 / 0) { + const writer = stream.writable.getWriter(); + writer.write(input).catch(() => { + }), writer.close().catch(() => { + }); + const chunks = []; + let length = 0; + const reader = stream.readable.getReader(); + for (; ; ) { + const { value, done } = await reader.read(); + if (done) + break; + if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength) + throw new JWEInvalid("Decompressed plaintext exceeded the configured limit"); + } + return concat(...chunks); +} +async function compress(input) { + return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input); +} +async function decompress(input, maxLength) { + return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength); +} +export { + compress, + decompress, + validateZip +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 000000000..a87af4eb5 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js @@ -0,0 +1,116 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const wrap = [ + ["encrypt", "wrapKey"], + ["decrypt", "unwrapKey"] +], derive = [[], ["deriveBits"]], none = [[], []]; +function rsaes(bits) { + return { + kty: ["RSA"], + mode: "key-encryption", + subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` }, + usages: wrap, + ops: ["wrapKey", "unwrapKey"] + }; +} +function ecdh(mode) { + return { + kty: ["EC", "OKP"], + mode, + subtle: { name: "ECDH" }, + resolve: ({ kty, crv, asymmetricKeyType }) => { + if (crv === "X25519" || asymmetricKeyType === "x25519") + return { name: "X25519" }; + if (kty === "OKP") + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + return { name: "ECDH", namedCurve: crv }; + }, + usages: derive, + ops: [void 0, "deriveBits"] + }; +} +function aeskw(bits, gcm = !1) { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits }, + usages: none, + ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"] + }; +} +function pbes2() { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: "PBKDF2" }, + usages: none, + ops: ["deriveBits", "deriveBits"] + }; +} +const JWE = table({ + dir: { + kty: ["oct"], + mode: "direct-encryption", + secret: !0, + subtle: { name: "AES-GCM" }, + usages: none, + ops: ["encrypt", "decrypt"] + }, + "RSA-OAEP": rsaes(1), + "RSA-OAEP-256": rsaes(256), + "RSA-OAEP-384": rsaes(384), + "RSA-OAEP-512": rsaes(512), + "ECDH-ES": ecdh("direct-key-agreement"), + "ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"), + A128KW: aeskw(128), + A192KW: aeskw(192), + A256KW: aeskw(256), + A128GCMKW: aeskw(128, !0), + A192GCMKW: aeskw(192, !0), + A256GCMKW: aeskw(256, !0), + "PBES2-HS256+A128KW": pbes2(), + "PBES2-HS384+A192KW": pbes2(), + "PBES2-HS512+A256KW": pbes2() +}), contentOps = ["encrypt", "decrypt"]; +function contentEncryption(bits, cbc = !1) { + return { + kty: ["oct"], + secret: !0, + subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits }, + usages: none, + ops: contentOps, + cekBits: bits, + ivBits: cbc ? 128 : 96, + cbc + }; +} +const ENC = table({ + A128GCM: contentEncryption(128), + A192GCM: contentEncryption(192), + A256GCM: contentEncryption(256), + "A128CBC-HS256": contentEncryption(256, !0), + "A192CBC-HS384": contentEncryption(384, !0), + "A256CBC-HS512": contentEncryption(512, !0) +}); +function unsupported(parameter, name) { + throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`); +} +function jweAlgorithm(alg) { + return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm"); +} +function isJWECEKTransport(algorithm) { + return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping"; +} +function jweEncryption(enc) { + return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm"); +} +export { + JWE, + isJWECEKTransport, + jweAlgorithm, + jweEncryption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 000000000..e5453b9ba --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js @@ -0,0 +1,187 @@ +import { decrypt, generateCek } from "./content_encryption.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js"; +import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decryptKeyManagement } from "./key_management.js"; +import { decoder } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { decompress, validateZip } from "./deflate.js"; +function snapshotSharedJWE(jwe) { + const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; + if (iv !== void 0 && (typeof iv != "string" || !iv)) + throw new JWEInvalid("JWE Initialization Vector incorrect type"); + if (typeof ciphertext != "string") + throw new JWEInvalid("JWE Ciphertext missing or incorrect type"); + if (tag !== void 0 && (typeof tag != "string" || !tag)) + throw new JWEInvalid("JWE Authentication Tag incorrect type"); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWEInvalid("JWE Protected Header incorrect type"); + if (aad !== void 0 && (typeof aad != "string" || !aad)) + throw new JWEInvalid("JWE AAD incorrect type"); + if (unprotected !== void 0 && !isObject(unprotected)) + throw new JWEInvalid("JWE Shared Unprotected Header incorrect type"); + return { + aad, + ciphertext, + iv, + protected: encodedProtected, + tag, + unprotected: unprotected === void 0 ? void 0 : { ...unprotected } + }; +} +function snapshotRecipientJWE(recipient) { + let header, headerAlg; + try { + const { header: inputHeader } = recipient; + if (isObject(inputHeader)) { + headerAlg = inputHeader.alg; + const parameters = Object.keys(inputHeader); + parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [ + parameter, + parameter === "alg" ? headerAlg : inputHeader[parameter] + ])); + } else + header = inputHeader; + const { encrypted_key: encryptedKey } = recipient; + return [{ encrypted_key: encryptedKey, header }, headerAlg]; + } catch (error) { + return [void 0, headerAlg, error]; + } +} +function checkRecipient(jwe) { + const { encrypted_key: encryptedKey, header } = jwe; + if (encryptedKey !== void 0 && typeof encryptedKey != "string") + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type"); + if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0) + throw new JWEInvalid("JOSE Header missing"); +} +function shareJWE(jwe) { + const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; + let parsedProt; + return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [ + parsedProt, + decodeBase64url(ciphertext, "ciphertext", JWEInvalid), + iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0, + tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0, + encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid) + ]; +} +function prepareDecrypt(options) { + return [ + options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms), + options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms), + options?.crit, + options?.maxPBES2Count, + options?.maxDecompressedLength + ]; +} +async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) { + const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe; + let joseHeader; + if (header !== void 0 || unprotected !== void 0) { + if (!isDisjoint(parsedProt, header, unprotected)) + throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint"); + joseHeader = { ...parsedProt, ...header, ...unprotected }; + } else + joseHeader = parsedProt ?? {}; + const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe; + validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header"); + const selected = JWE[alg]; + if (encodedKey === "" && (!selected || !isJWECEKTransport(selected))) + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + const integrated = selected?.mode === "integrated-encryption"; + if (!integrated && (typeof enc != "string" || !enc)) + throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header"); + if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2")) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + let encEntry; + if (integrated) { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (iv?.byteLength) + throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption"); + if (tag?.byteLength) + throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption"); + } else { + if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) + throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); + encEntry = jweEncryption(enc); + } + let encryptedKey; + if (encodedKey !== void 0) + try { + encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid); + } catch (error) { + if (!selected || !isJWECEKTransport(selected)) + throw error; + encryptedKey = new Uint8Array(); + } + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0); + const algEntry = selected ?? jweAlgorithm(alg); + isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array()); + const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt"); + let plaintext; + if (algEntry.mode === "integrated-encryption") + plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader); + else { + const encryption = encEntry; + let cek; + try { + cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption)); + } catch (err) { + if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) + throw err; + cek = generateCek(encryption); + } + plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData); + } + if (joseHeader.zip === "DEF") { + const decompressionLimit = maxDecompressedLength ?? 25e4; + if (decompressionLimit === 0) + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); + if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) + throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity"); + plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { + throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause }); + }); + } + return { + plaintext, + ...parsedProt && { protectedHeader: parsedProt }, + ...aad !== void 0 && { + additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid) + }, + ...unprotected && { sharedUnprotectedHeader: unprotected }, + ...header && { unprotectedHeader: header }, + ...resolvedKey && { key: k } + }; +} +async function decryptCompact(jwe, shared, key) { + if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string") + throw new JWEInvalid("Compact JWE must be a string or Uint8Array"); + const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split("."); + if (length !== 5) + throw new JWEInvalid("Invalid Compact JWE"); + return decryptJWE({ + ciphertext, + iv: iv || void 0, + protected: protectedHeader, + tag: tag || void 0, + encrypted_key: encryptedKey || void 0 + }, shared, key); +} +export { + checkRecipient, + decryptCompact, + decryptJWE, + prepareDecrypt, + shareJWE, + snapshotRecipientJWE, + snapshotSharedJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 000000000..5ab33766b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js @@ -0,0 +1,75 @@ +import { encode as b64u } from "../util/base64url.js"; +import { encrypt } from "./content_encryption.js"; +import { encryptKeyManagement } from "./key_management.js"; +import { JWEInvalid } from "../util/errors.js"; +import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js"; +import { encode } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { compress, validateZip } from "./deflate.js"; +function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { + if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) + throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint"); +} +function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) { + if (!input[1] && !input[2] && !input[3]) + throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()"); + options !== void 0 && (input[8] = options?.crit); + let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input; + if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters)) + throw new TypeError("JWE Key Management Parameters must be an object"); + checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); + const joseHeader = { + ...protectedHeader, + ...unprotectedHeader, + ...sharedUnprotectedHeader + }; + validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); + const algEntry = JWE[alg]; + if (algEntry?.mode === "integrated-encryption") { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (cek !== void 0) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + if (iv !== void 0) + throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + return [joseHeader, void 0, algEntry]; + } + if (typeof enc != "string" || !enc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); + return [joseHeader, jweEncryption(enc), algEntry]; +} +async function encryptJWE(input, checked, key) { + const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input; + let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader; + const algEntry = selected ?? jweAlgorithm(joseHeader.alg); + let encryptedKey, parameters, cek; + algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)); + const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS); + let plaintext = inputPlaintext; + joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => { + throw new JWEInvalid("Failed to compress plaintext", { cause }); + })); + let ciphertext, tag, iv; + algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); + const jwe = { + ciphertext: b64u(ciphertext) + }; + return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe; +} +async function createJWE(input, key, options) { + return encryptJWE(input, checkEncryptHeaders(input, options), key); +} +function compactJWE(jwe) { + return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join("."); +} +export { + checkDisjoint, + checkEncryptHeaders, + compactJWE, + createJWE, + encryptJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js new file mode 100644 index 000000000..8a4d3fc93 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js @@ -0,0 +1,15 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { JWS } from "./jws_algorithms.js"; +import { JWE } from "./jwe_algorithms.js"; +const algArgument = '"alg" (Algorithm)'; +function unsupportedAlg(source = 'JWK "alg" (Algorithm) Parameter') { + throw new JOSENotSupported(`Invalid or unsupported ${source} value`); +} +function keyAlgorithm(alg, source) { + return (typeof alg == "string" ? JWS[alg] ?? JWE[alg] : void 0) ?? unsupportedAlg(source); +} +export { + algArgument, + keyAlgorithm, + unsupportedAlg +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 000000000..7b7533b70 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js @@ -0,0 +1,196 @@ +import { encode as b64u } from "../util/base64url.js"; +import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js"; +import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js"; +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decodeBase64url, assertUint8Array, isObject } from "./validate.js"; +import { digest, concat, encode, uint32be } from "./buffer_utils.js"; +import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js"; +function checkEcdhCryptoKey(key, usage) { + if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") + throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519"); + checkUsage(key, usage); +} +async function aeskwWrap(alg, key, cek) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW")); +} +async function aeskwUnwrap(alg, key, encryptedKey) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek)); +} +function checkRsaKey(alg, key, usage) { + checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key); +} +async function deriveKey(p2s, alg, p2c, key) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) + throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets"); + if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) + throw new JWEInvalid("PBES2 Count Input must be a positive integer"); + const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: "PBKDF2", + salt + }, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits"); + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); +} +function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +async function concatKdf(Z, L, OtherInfo) { + const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen); + for (let i = 1; i <= reps; i++) { + const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo)); + dk.set(hashResult, (i - 1) * hashLen); + } + return dk.slice(0, dkLen); +} +async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) { + checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits"); + const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({ + name: publicKey.algorithm.name, + public: publicKey + }, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(Z, keyLength, otherInfo); +} +function assertEcdhKey(key) { + assertCryptoKey(key); + const curve = key.algorithm.namedCurve; + if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519") + throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime"); +} +function partyInfo(joseHeader, name) { + const value = joseHeader[name]; + if (value !== void 0) { + if (typeof value != "string") + throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`); + return decodeBase64url(value, name, JWEInvalid); + } +} +function checkPartyInfo(apu, apv) { + if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) { + for (let i = 0; i < apu.byteLength; i++) + if (apu[i] !== apv[i]) + return; + throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct'); + } +} +function assertEncryptedKey(encryptedKey) { + if (encryptedKey === void 0) + throw new JWEInvalid("JWE Encrypted Key missing"); +} +function assertNoEncryptedKey(encryptedKey) { + if (encryptedKey !== void 0) + throw new JWEInvalid("Encountered unexpected JWE Encrypted Key"); +} +function validateMaxPBES2Count(value) { + if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1)) + throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity"); +} +async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) { + const { alg } = entry, mode = entry.mode; + if (mode === "direct-encryption") + return assertNoEncryptedKey(encryptedKey), key; + const direct = mode === "direct-key-agreement"; + switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) { + case "ECDH": { + const { epk } = joseHeader; + if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter))) + throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid'); + assertEcdhKey(key); + const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv"); + checkPartyInfo(partyUInfo, partyVInfo); + const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (direct) + return sharedSecret; + key = sharedSecret; + break; + } + case "RSA-OAEP": + return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey)); + case "PBKDF2": { + if (typeof joseHeader.p2c != "number") + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid'); + validateMaxPBES2Count(maxPBES2Count); + const p2cLimit = maxPBES2Count ?? 1e4; + if (joseHeader.p2c > p2cLimit) + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds'); + if (typeof joseHeader.p2s != "string") + throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid'); + const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid); + key = await deriveKey(p2s, alg, joseHeader.p2c, key); + break; + } + case "AES-GCM": { + if (typeof joseHeader.iv != "string") + throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid'); + if (typeof joseHeader.tag != "string") + throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid'); + const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid); + if (iv.byteLength !== 12) + throw new JWEInvalid("Invalid Initialization Vector length"); + if (tag.byteLength !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); + } + } + return aeskwUnwrap(alg.slice(-6), key, encryptedKey); +} +async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) { + const { alg, mode } = entry, transport = isJWECEKTransport(entry); + if (providedCek !== void 0 && !transport) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt"); + if (mode === "direct-encryption") + return [key, void 0, void 0]; + const cek = transport ? providedCek ?? generateCek(enc) : void 0; + cek && checkCekLength(cek, enc.cekBits); + let encryptedKey, parameters; + switch (entry.subtle.name) { + case "ECDH": { + assertEcdhKey(key); + const { apu: providedApu, apv: providedApv } = providedParameters; + providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"'); + const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv"); + checkPartyInfo(apu, apv); + let ephemeralKey; + providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey; + const subtle = crypto.subtle; + let exportableEpk = ephemeralKey; + if (!exportableEpk.extractable) { + if (typeof subtle.getPublicKey != "function") + throw new TypeError('CryptoKey for "epk" must be extractable'); + exportableEpk = await subtle.getPublicKey(ephemeralKey, []); + } + const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty }; + if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct) + return [sharedSecret, void 0, parameters]; + key = sharedSecret; + break; + } + case "RSA-OAEP": { + assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek)); + break; + } + case "PBKDF2": { + const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; + key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) }; + break; + } + case "AES-GCM": { + const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv; + if (!(iv instanceof Uint8Array)) + throw new TypeError('"iv" must be an instance of Uint8Array'); + const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); + encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; + } + } + if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength) + throw new TypeError("JWE key management algorithm did not produce an Encrypted Key"); + return [cek, encryptedKey, parameters]; +} +export { + decryptKeyManagement, + encryptKeyManagement, + validateMaxPBES2Count +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js new file mode 100644 index 000000000..a4b277f18 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js @@ -0,0 +1,33 @@ +import { decode as b64u } from "./base64url.js"; +import { strictDecoder } from "../lib/buffer_utils.js"; +import { isObject } from "../lib/validate.js"; +import { JWTInvalid } from "./errors.js"; +function decodeJwt(jwt) { + if (typeof jwt != "string") + throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string"); + const { 1: payload, length } = jwt.split("."); + if (length === 5) + throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded"); + if (length !== 3) + throw new JWTInvalid("Invalid JWT"); + if (!payload) + throw new JWTInvalid("JWTs must contain a payload"); + let decoded; + try { + decoded = b64u(payload); + } catch { + throw new JWTInvalid("Failed to base64url decode the payload"); + } + let result; + try { + result = JSON.parse(strictDecoder.decode(decoded)); + } catch { + throw new JWTInvalid("Failed to parse the decoded payload as JSON"); + } + if (!isObject(result)) + throw new JWTInvalid("Invalid JWT Claims Set"); + return result; +} +export { + decodeJwt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js new file mode 100644 index 000000000..274e03617 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js @@ -0,0 +1,19 @@ +import { parseJoseHeader } from "../lib/validate.js"; +function decodeProtectedHeader(token) { + let protectedB64u; + if (typeof token == "string") { + const parts = token.split("."); + (parts.length === 3 || parts.length === 5) && ([protectedB64u] = parts); + } else if (typeof token == "object" && token) + if ("protected" in token) + protectedB64u = token.protected; + else + throw new TypeError("Token does not contain a Protected Header"); + const invalid = "Invalid Token or Protected Header formatting"; + if (typeof protectedB64u != "string" || !protectedB64u) + throw new TypeError(invalid); + return parseJoseHeader(protectedB64u, TypeError, invalid); +} +export { + decodeProtectedHeader +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json new file mode 100644 index 000000000..2d7d92f72 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json @@ -0,0 +1,134 @@ +{ + "name": "jose", + "version": "6.2.12", + "description": "JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes", + "keywords": [ + "akp", + "browser", + "bun", + "cloudflare", + "compact", + "decode", + "decrypt", + "deno", + "detached", + "ec", + "ecdsa", + "ed25519", + "eddsa", + "edge", + "electron", + "embedded", + "encrypt", + "flattened", + "general", + "jose", + "json web token", + "jsonwebtoken", + "jwa", + "jwe", + "jwk", + "jwks", + "jws", + "jwt-decode", + "jwt", + "ml-dsa", + "netlify", + "next", + "nextjs", + "oct", + "okp", + "payload", + "pem", + "pkcs8", + "rsa", + "sign", + "signature", + "spki", + "validate", + "vercel", + "verify", + "webcrypto", + "workerd", + "workers", + "x509" + ], + "homepage": "https://github.com/panva/jose", + "repository": "panva/jose", + "funding": { + "url": "https://github.com/sponsors/panva" + }, + "license": "MIT", + "author": "Filip Skokan ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/webapi/index.js" + }, + "./jwk/*": { + "types": "./dist/types/jwk/*.d.ts", + "default": "./dist/webapi/jwk/*.js" + }, + "./key/import": { + "types": "./dist/types/key/import.d.ts", + "default": "./dist/webapi/key/import.js" + }, + "./key/export": { + "types": "./dist/types/key/export.d.ts", + "default": "./dist/webapi/key/export.js" + }, + "./key/generate/keypair": { + "types": "./dist/types/key/generate_key_pair.d.ts", + "default": "./dist/webapi/key/generate_key_pair.js" + }, + "./key/generate/secret": { + "types": "./dist/types/key/generate_secret.d.ts", + "default": "./dist/webapi/key/generate_secret.js" + }, + "./jwks/*": { + "types": "./dist/types/jwks/*.d.ts", + "default": "./dist/webapi/jwks/*.js" + }, + "./jwt/decode": { + "types": "./dist/types/util/decode_jwt.d.ts", + "default": "./dist/webapi/util/decode_jwt.js" + }, + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./decode/protected_header": { + "types": "./dist/types/util/decode_protected_header.d.ts", + "default": "./dist/webapi/util/decode_protected_header.js" + }, + "./jws/*": { + "types": "./dist/types/jws/*.d.ts", + "default": "./dist/webapi/jws/*.js" + }, + "./jwe/*": { + "types": "./dist/types/jwe/*.d.ts", + "default": "./dist/webapi/jwe/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + }, + "./base64url": { + "types": "./dist/types/util/base64url.d.ts", + "default": "./dist/webapi/util/base64url.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/webapi/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/webapi/**/*.js", + "dist/types/**/*.d.ts", + "!dist/**/*.bundle.js", + "!dist/**/*.umd.js", + "!dist/**/*.min.js", + "!dist/types/lib/**" + ] +} diff --git a/tests/corpus/6332-fluent-this-declaration/main.ts b/tests/corpus/6332-fluent-this-declaration/main.ts new file mode 100644 index 000000000..10804083f --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/main.ts @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s: Signer): string { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/main.mjs b/tests/corpus/6333-fluent-this-declaration-js/main.mjs new file mode 100644 index 000000000..44ac698cc --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/main.mjs @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s) { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6334-discarded-logical-statements.mjs b/tests/corpus/6334-discarded-logical-statements.mjs new file mode 100644 index 000000000..2e1df91a8 --- /dev/null +++ b/tests/corpus/6334-discarded-logical-statements.mjs @@ -0,0 +1,42 @@ +// Minified `a && b;` / `a || b;` statements whose operands share no value +// representation lower as the control flow they are (jose's secs()), and +// element reads on a RegExpExecArray | null the code ruled non-null +// without narrowing answer undefined for groups that did not participate. +const multipliers = { s: 1, m: 60, h: 3600, d: 86400 }; +const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|s|minutes?|m|hours?|h|days?|d)(?: (ago|from now))?$/i; +function invalid() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalid(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalid(); + const value = parseFloat(matched[2]); + const n = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(n) || invalid(), matched[1] === "-" || matched[4] === "ago" ? -n : n; +} +for (const input of ["10 minutes", "2h", "-1d", "+3 s", "1.5 hours ago", "4 days from now", "-2h ago", "bogus", "", 7]) { + try { + console.log(JSON.stringify(input), secs(input)); + } catch (e) { + console.log(JSON.stringify(input), e.name, e.message); + } +} + +const log = []; +function note(v) { + log.push(v); + return v; +} +const values = [0, 1, "", "x", undefined, null, true, false]; +for (const v of values) { + (note(v) || typeof v === "number") && note("and"); + (v && typeof v === "string") || note("or"); + !v || (v === true && note("bool")) || note("rest"); +} +console.log(JSON.stringify(log)); + +const m = /(a)?(b)(c)?/.exec("b"); +console.log(m[1], m[2], m[3], m[7], m[1] === undefined, m.length); +const none = /(z)/.exec("b"); +console.log(none?.[1], none === null); diff --git a/tests/corpus/6335-discarded-logical-statements.ts b/tests/corpus/6335-discarded-logical-statements.ts new file mode 100644 index 000000000..84b89f7b1 --- /dev/null +++ b/tests/corpus/6335-discarded-logical-statements.ts @@ -0,0 +1,27 @@ +// `a && b;` / `a || b;` statements whose operands share no value +// representation lower as control flow: only the deciding test runs. +function invalid(): never { + throw new TypeError("invalid"); +} +function check(input: string): string { + const matched = /^(\+|-)?(\d+)(s|m)( ago)?$/.exec(input); + (!matched || (matched[4] && matched[1])) && invalid(); + return matched ? `${matched[2]}${matched[3]}` : "none"; +} +for (const input of ["5s", "-5m", "5m ago", "-5m ago", "x"]) { + try { + console.log(input, check(input)); + } catch (e) { + console.log(input, (e as Error).message); + } +} +const seen: string[] = []; +function flag(s: string, v: boolean | string | undefined): boolean | string | undefined { + seen.push(s); + return v; +} +for (const v of [true, false, "", "y", undefined]) { + (flag("a", v) || flag("b", v === undefined ? "u" : false)) && seen.push("then"); + (flag("c", v) && flag("d", typeof v === "string")) || seen.push("else"); +} +console.log(seen.join(",")); diff --git a/tests/corpus/6340-prototype-reflection-optional-view-param.ts b/tests/corpus/6340-prototype-reflection-optional-view-param.ts new file mode 100644 index 000000000..58e21164d --- /dev/null +++ b/tests/corpus/6340-prototype-reflection-optional-view-param.ts @@ -0,0 +1,42 @@ +// A generic method whose optional parameter is an interface view (or an +// erased type parameter) reflects through the prototype of an instance +// that crossed into checked-dynamic code, and calls directly with the +// argument omitted. +interface Handle { + close(): string; +} +class Plain implements Handle { + close(): string { + return "closed"; + } +} +class Registry { + private current: Handle | null = null; + use(handle: Handle | null): void { + this.current = handle; + } + wrap(fn: F, handle?: Handle | null): F { + if (handle) this.current = handle; + return fn; + } + tag(value: T, label?: T): string { + return `${String(value)}:${label === undefined ? "none" : String(label)}`; + } + size(): number { + return this.current === null ? 0 : 1; + } +} + +const registry = new Registry(); +const inc = registry.wrap((x: number) => x + 1); +console.log(inc(1), registry.size()); +const dec = registry.wrap((x: number) => x - 1, new Plain()); +console.log(dec(1), registry.size()); +console.log(registry.tag(1), registry.tag("a", "b")); + +const boxed: unknown = registry; +const proto = Object.getPrototypeOf(boxed); +console.log(Object.getOwnPropertyNames(proto).join(",")); +console.log(typeof proto.wrap, typeof proto.tag, proto.size.call(registry)); +const loose: any = registry; +console.log(loose.size(), proto.tag.call(registry, 2), proto.tag.call(registry, 2, 3)); diff --git a/tests/corpus/6350-globalthis-assertion-state.ts b/tests/corpus/6350-globalthis-assertion-state.ts new file mode 100644 index 000000000..4a7f5a990 --- /dev/null +++ b/tests/corpus/6350-globalthis-assertion-state.ts @@ -0,0 +1,50 @@ +// Process-global state on the global object through a type assertion: +// every assertion of globalThis denotes the global object itself, so a +// write through one view is visible through every other view and through +// a view bound before the write. +const KEY = Symbol.for("example.state"); +interface State { + n: number; + log: string[]; +} +type Holder = { [KEY]?: State; label?: string }; + +const early = globalThis as Holder; + +function get(): State | undefined { + return early[KEY]; +} + +early[KEY] = { n: 1, log: [] }; +console.log(get()?.n, (globalThis as Holder)[KEY]?.n); + +function touch(entry: string): void { + const view = globalThis as Holder; + const state = view[KEY] ?? (view[KEY] = { n: 0, log: [] }); + state.n++; + state.log.push(entry); +} +touch("a"); +touch("b"); +console.log(early[KEY]?.n, early[KEY]?.log.join(","), get() === early[KEY]); + +// string-keyed members of the same object +(globalThis as Holder).label = "ready"; +console.log(early.label, (globalThis as unknown as { label?: string }).label); + +// ??= initializes once; delete removes the member for every view +const COUNTER = Symbol.for("example.counter"); +type Counters = { [COUNTER]?: { hits: number } }; +(globalThis as Counters)[COUNTER] ??= { hits: 0 }; +(globalThis as Counters)[COUNTER] ??= { hits: 100 }; +const counters = globalThis as Counters; +counters[COUNTER]!.hits += 2; +console.log((globalThis as Counters)[COUNTER]?.hits, COUNTER in globalThis); +delete counters[COUNTER]; +console.log((globalThis as Counters)[COUNTER], COUNTER in globalThis); + +// an index-signature view +const dict = globalThis as unknown as Record; +dict.exampleTotal = 1; +dict["exampleTotal"] = (dict["exampleTotal"] ?? 0) + 1; +console.log((globalThis as unknown as { exampleTotal?: number }).exampleTotal); diff --git a/tests/corpus/6352-globalthis-state-modules/boot.ts b/tests/corpus/6352-globalthis-state-modules/boot.ts new file mode 100644 index 000000000..856ff1baa --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/boot.ts @@ -0,0 +1,14 @@ +// The boot module writes the state through its own assertion of globalThis. +const STATE = Symbol.for("example.boot-state"); +const PENDING = Symbol.for("example.boot-pending"); + +interface Early { + startedAt: number; + steps: string[]; +} + +export function boot(): void { + const view = globalThis as { [STATE]?: Early; [PENDING]?: Promise }; + view[STATE] = { startedAt: 42, steps: ["boot"] }; + view[PENDING] = Promise.resolve("container-1"); +} diff --git a/tests/corpus/6352-globalthis-state-modules/main.ts b/tests/corpus/6352-globalthis-state-modules/main.ts new file mode 100644 index 000000000..5b5aee7eb --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/main.ts @@ -0,0 +1,18 @@ +// Boot writes the state; main reads it through a binding exported by +// another module, then takes the pending value out of the global object. +import { boot } from "./boot.ts"; +import { PENDING_KEY, STATE_KEY, globalState, readState } from "./state.ts"; + +console.log(readState() === undefined); +boot(); +const state = readState(); +console.log(state?.startedAt, state?.steps.join(",")); +state?.steps.push("main"); +console.log(globalState[STATE_KEY]?.steps.join(",")); + +async function main(): Promise { + const pending = globalState[PENDING_KEY]; + globalState[PENDING_KEY] = undefined; + console.log(await pending, globalState[PENDING_KEY]); +} +void main(); diff --git a/tests/corpus/6352-globalthis-state-modules/state.ts b/tests/corpus/6352-globalthis-state-modules/state.ts new file mode 100644 index 000000000..cef92c25f --- /dev/null +++ b/tests/corpus/6352-globalthis-state-modules/state.ts @@ -0,0 +1,18 @@ +// Shared by the boot and main modules: the state lives on the global +// object, keyed by registered symbols. +export const STATE_KEY = Symbol.for("example.boot-state"); +export const PENDING_KEY = Symbol.for("example.boot-pending"); + +export interface BootState { + startedAt: number; + steps: string[]; +} + +export const globalState = globalThis as { + [STATE_KEY]?: BootState; + [PENDING_KEY]?: Promise; +}; + +export function readState(): BootState | undefined { + return globalState[STATE_KEY]; +} diff --git a/tests/corpus/key-order-late-optional.ts b/tests/corpus/key-order-late-optional.ts new file mode 100644 index 000000000..f4742acb0 --- /dev/null +++ b/tests/corpus/key-order-late-optional.ts @@ -0,0 +1,84 @@ +// An optional key that no literal creates is appended when it is first +// assigned, after every key the value was built with, and straight-line +// assignments of several such keys keep their program order, including +// assignments behind conditions. Assigning a key that already exists never +// moves it. +interface Item { + label?: string; + id: number; + done: boolean; +} +function makeItem(id: number): Item { + return { id, done: false }; +} +const item = makeItem(1); +item.label = "first"; +console.log(Object.keys(item).join(","), JSON.stringify(item)); +console.log(item); +const direct: Item = { id: 2, done: true }; +direct.label = "second"; +console.log(Object.keys(direct).join(","), JSON.stringify(direct)); +const untouched = makeItem(3); +console.log(JSON.stringify(untouched), Object.keys(untouched).join(",")); + +// Two late keys assigned in reverse declaration order. +interface Pos { + id: number; + x?: number; + y?: number; +} +function makePos(): Pos { + return { id: 0 }; +} +const pos = makePos(); +pos.y = 1; +pos.x = 2; +console.log(Object.keys(pos).join(","), JSON.stringify(pos)); +console.log(pos); +pos.id = 3; +console.log(JSON.stringify(pos)); + +// An options object filled key by key, some keys conditionally. +interface Opts { + verbose?: boolean; + depth?: number; + name?: string; +} +function build(deep: boolean, loud: boolean): Opts { + const o: Opts = {}; + o.name = "n"; + if (deep) o.depth = 2; + if (loud) o.verbose = true; + return o; +} +for (const deep of [true, false]) + for (const loud of [true, false]) console.log(JSON.stringify(build(deep, loud))); +console.log(build(true, true)); +const forIn: string[] = []; +for (const k in build(true, true)) forIn.push(k); +console.log(forIn.join(",")); + +// Late keys on records stored in arrays and Maps. +interface Task { + owner?: string; + title: string; + priority: number; +} +const tasks: Task[] = []; +for (let i = 0; i < 3; i++) tasks.push({ title: "t" + i, priority: i }); +for (const t of tasks) if (t.priority > 0) t.owner = "o" + t.priority; +console.log(JSON.stringify(tasks)); +const byTitle = new Map(); +for (const t of tasks) byTitle.set(t.title, t); +console.log(byTitle); + +// An explicit undefined is a present key at its literal position. +interface Maybe { + a?: number; + b: number; +} +function makeMaybe(): Maybe { + return { b: 1, a: undefined }; +} +console.log(Object.keys(makeMaybe()).join(","), JSON.stringify(makeMaybe())); +console.log(makeMaybe()); diff --git a/tests/corpus/key-order-literal-flows.ts b/tests/corpus/key-order-literal-flows.ts new file mode 100644 index 000000000..350e78ffa --- /dev/null +++ b/tests/corpus/key-order-literal-flows.ts @@ -0,0 +1,150 @@ +// Records enumerate their keys in the order their object literals create +// them, even when every literal writes the keys in another order than the +// type declares them and the value is observed far from the literal: +// factory returns, arrays, Maps, nested records, generic factories, union +// arms, records widened to unknown/any, structuredClone, and every +// order-observing surface (keys, values, entries, for...in, JSON.stringify +// with replacers and indentation, inspect, console.log, util.format). +import { format, inspect } from "node:util"; + +interface Point { + x: number; + y: number; + label: string; +} +function makePoint(i: number): Point { + return { label: "p" + i, y: i * 2, x: i }; +} + +const p = makePoint(1); +console.log(Object.keys(p).join(",")); +console.log(Object.values(p).join(",")); +console.log(JSON.stringify(Object.entries(p))); +const forIn: string[] = []; +for (const k in p) forIn.push(k); +console.log(forIn.join(",")); +console.log(JSON.stringify(p)); +console.log(JSON.stringify(p, null, 2)); +console.log(JSON.stringify(p, ["y", "x"])); +const visited: string[] = []; +JSON.stringify(p, (key: string, value: unknown) => { + visited.push(key); + return value; +}); +console.log(visited.join(",")); +console.log(p); +console.log(inspect(p)); +console.log(format("%o|%j|%O", p, p, p)); + +// Arrays of records: literals pushed in a loop and produced by map. +interface Row { + id: number; + name: string; + score: number; +} +const rows: Row[] = []; +for (let i = 0; i < 3; i++) rows.push({ name: "n" + i, score: i * 10, id: i }); +for (const r of rows) console.log(Object.values(r).join("|")); +console.log(JSON.stringify(rows)); +const mapped: Row[] = [4, 5].map((i) => ({ name: "m" + i, score: i, id: i * 100 })); +console.log(JSON.stringify(mapped)); +console.log(mapped); + +// Records stored in a Map, read back, serialized and inspected. +const byName = new Map(); +for (const r of rows) byName.set(r.name, r); +byName.set("extra", { name: "extra", score: 9, id: 99 }); +console.log(byName); +console.log(JSON.stringify([...byName.values()])); +console.log(Object.keys(byName.get("extra")!).join(",")); + +// Nested records: every level keeps its own literal's order. +interface Inner { + b: number; + a: number; +} +interface Outer { + inner: Inner; + id: number; + tags: string[]; +} +function makeOuter(id: number): Outer { + return { id, tags: ["t" + id], inner: { a: id, b: id + 1 } }; +} +console.log(JSON.stringify(makeOuter(1))); +console.log(makeOuter(2)); +console.log(JSON.stringify([makeOuter(3), makeOuter(4)], null, 1)); + +// Generic factories instantiate per type argument. +function boxed(value: T): { value: T; tag: string; count: number } { + return { tag: "box", count: 1, value }; +} +console.log(JSON.stringify(boxed(7))); +console.log(JSON.stringify(boxed("s"))); +console.log(Object.keys(boxed(true)).join(",")); + +// Union arms keep their own literal orders. +type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number }; +function shapeAt(i: number): Shape { + return i % 2 === 0 ? { r: i, kind: "circle" } : { h: i, kind: "rect", w: i + 1 }; +} +const shapes: Shape[] = [shapeAt(0), shapeAt(1), shapeAt(2)]; +for (const s of shapes) console.log(JSON.stringify(s)); +console.log(shapes); + +// Widened to unknown and any, and cloned. +const u: unknown = makePoint(3); +console.log(JSON.stringify(u)); +const loose: any = makePoint(4); +console.log(Object.keys(loose).join(",")); +const cloned = structuredClone(makePoint(5)); +console.log(JSON.stringify(cloned), Object.keys(cloned).join(",")); + +// Integer-like keys still come first, ascending, ahead of the literal order. +interface Indexed { + name: string; + "10": number; + "2": number; + "01": number; +} +function makeIndexed(): Indexed { + return { "01": 1, name: "n", "10": 10, "2": 2 }; +} +console.log(JSON.stringify(makeIndexed())); +console.log(Object.keys(makeIndexed()).join(",")); + +// A literal whose fields are flipped by later assignments: assigning an +// existing key never moves it (the pattern of scriptc's own +// RuntimeFeatures scan). +interface Features { + regex: boolean; + eventTarget: boolean; + warningReplace: boolean; + stdioExtended: boolean; + processEvents: boolean; + emitter: boolean; +} +function scanFeatures(classes: string[], workers: boolean): Features { + const features: Features = { + regex: false, + eventTarget: classes.some((c) => c === "EventTarget"), + processEvents: false, + warningReplace: false, + stdioExtended: false, + emitter: workers, + }; + if (classes.length > 1) features.regex = true; + features.stdioExtended = workers; + return features; +} +console.log(JSON.stringify(scanFeatures(["EventTarget", "X"], true))); +console.log(JSON.stringify(scanFeatures([], false))); + +// Literals written in declaration order keep it. +interface Ordered { + first: string; + second: number; + third: boolean; +} +const ordered: Ordered = { first: "f", second: 2, third: true }; +console.log(JSON.stringify(ordered), Object.keys(ordered).join(",")); diff --git a/tests/corpus/key-order-spread-copies.ts b/tests/corpus/key-order-spread-copies.ts new file mode 100644 index 000000000..b677eb0a4 --- /dev/null +++ b/tests/corpus/key-order-spread-copies.ts @@ -0,0 +1,107 @@ +// Copies keep the key order of the values they copy: a spread places the +// source's keys at the spread's position in the source's own order, an +// override keeps the position the spread gave it, and new keys follow. +// Rest bindings, width copies, Object.assign and structuredClone keep the +// source order too, including when the source literal was written in +// another order than its type declares. +interface Source { + a: number; + b: number; + c: number; +} +function makeSource(i: number): Source { + return { c: i, b: i + 1, a: i + 2 }; +} + +// Spread followed by a new key, and by an override. +interface Extended { + a: number; + b: number; + c: number; + d: number; +} +const extended: Extended = { ...makeSource(1), d: 4 }; +console.log(JSON.stringify(extended), Object.keys(extended).join(",")); +const overridden: Source = { ...makeSource(2), b: 0 }; +console.log(JSON.stringify(overridden)); +interface Led { + a: number; + b: number; + c: number; + lead: number; +} +const leading: Led = { lead: 0, ...makeSource(3) }; +console.log(JSON.stringify(leading)); + +// A spread whose overrides change field types builds a new shape through +// the field-by-field desugar (scriptc's own frontend-cache writer). +interface Entry { + ok: true; + llvm: string; + ir: string[] | null; + sidecarJson: string | null; + features: number; + sources: [string, string][]; + warnings?: string[]; +} +function makeEntry(): Entry { + return { ok: true, llvm: "L", ir: ["x"], sidecarJson: null, features: 1, sources: [] }; +} +const entry = makeEntry(); +console.log(JSON.stringify({ schema: "v3", input: { ...entry, llvm: "", ir: null } })); + +// Same-shape updates keep every key in place. +interface State { + name: string; + count: number; + last: string | undefined; +} +let state: State = { count: 0, last: undefined, name: "s" }; +for (let i = 0; i < 3; i++) state = { ...state, count: state.count + 1, last: "i" + i }; +console.log(JSON.stringify(state), Object.keys(state).join(",")); +console.log({ ...state, name: "shown" }); + +// Rest bindings copy the remaining keys in the source's order. +const { b, ...rest } = makeSource(5); +console.log(b, JSON.stringify(rest), Object.keys(rest).join(",")); +let picked: number; +let restAgain: { a: number; c: number }; +({ b: picked, ...restAgain } = makeSource(6)); +console.log(picked, JSON.stringify(restAgain), Object.keys(restAgain).join(",")); + +// Width copies into an optional-widened type and assignment into a fresh +// target. +interface Wider { + a: number; + b: number; + c: number; + note?: string; +} +function widen(s: Source): Wider { + return s; +} +console.log(JSON.stringify(widen(makeSource(7)))); +const assigned = Object.assign({ z: 0 }, makeSource(8)); +console.log(JSON.stringify(assigned)); +console.log(JSON.stringify(structuredClone(makeSource(9)))); + +// Nested spreads of nested records. +interface Holder { + inner: Source; + id: number; +} +const holder: Holder = { id: 1, inner: makeSource(10) }; +const copied: Holder = { ...holder, id: 2 }; +console.log(JSON.stringify(copied)); +console.log(copied); + +// Conditional spreads place their key at the spread's position. +interface Options { + mode: string; + depth?: number; + verbose: boolean; +} +function options(deep: boolean): Options { + return { mode: "m", ...(deep ? { depth: 3 } : {}), verbose: deep }; +} +console.log(JSON.stringify(options(true)), JSON.stringify(options(false))); diff --git a/tests/coverage-fixtures/divergences.ts b/tests/coverage-fixtures/divergences.ts index e13701a93..10b0a12e7 100644 --- a/tests/coverage-fixtures/divergences.ts +++ b/tests/coverage-fixtures/divergences.ts @@ -20,14 +20,14 @@ console.log(tagged.label); const view: Labeled = tagged; console.log(view === tagged); -// Key order: the literal's order differs from the declaration and the -// program observes it. +// Key order: values of one type are built in two orders and the program +// observes them, so the type keeps its declaration order. interface Point { x: number; y: number; } const point: Point = { y: 1, x: 2 }; -console.log(JSON.stringify(point)); +console.log(JSON.stringify(point), JSON.stringify({ x: 3, y: 4 })); // Text outside the built-in collation compares by code point natively. console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша")); diff --git a/tests/coverage-fixtures/dynamic-mix.ts b/tests/coverage-fixtures/dynamic-mix.ts index 0c9779a64..d70141b91 100644 --- a/tests/coverage-fixtures/dynamic-mix.ts +++ b/tests/coverage-fixtures/dynamic-mix.ts @@ -1,9 +1,9 @@ // Native checked values and loose equality share this program with -// island-backed APIs (SC2012) with native compilation refusals. +// native compilation refusals (a default-locale toLocaleString, SC2020). const v: any = 21; const doubled = v * 2; const root = Math.cbrt(81); -const up = (19.99).toPrecision(3); +const up = (19.99).toLocaleString(); const parsed = Number.parseFloat("1.5"); // a string argument compiles statically, like the global const unknownScore: unknown = 81; const flags = unknownScore == 81; diff --git a/tests/coverage-fixtures/mixed.ts b/tests/coverage-fixtures/mixed.ts index 73a36ce47..1ae9db26d 100644 --- a/tests/coverage-fixtures/mixed.ts +++ b/tests/coverage-fixtures/mixed.ts @@ -10,7 +10,7 @@ const handler = (x: number) => x * 2; const anyone: unknown = score(1); const flags = anyone == score(2); // unknown/object coercion keeps the fence; static primitive pairs lower -const { length: firstItem } = items; // object patterns over arrays keep the fence (patterns and defaults compile) +const { length: firstItem } = score(3) > 99 ? items : "abc"; // a union source keeps the object-pattern fence (narrow first) let label = "score: "; label += score(10); console.log(label); diff --git a/tests/coverage-fixtures/no-divergences.ts b/tests/coverage-fixtures/no-divergences.ts index 9571add6e..3739336e3 100644 --- a/tests/coverage-fixtures/no-divergences.ts +++ b/tests/coverage-fixtures/no-divergences.ts @@ -1,6 +1,6 @@ // Ordinary code: identity-preserving calls, declaration-ordered literals, -// Latin-script comparisons, related-class casts and ISO dates report no -// divergence. +// Latin-script comparisons, related-class casts, ISO dates and views of the +// global object report no divergence. interface Item { name: string; qty: number; @@ -32,3 +32,9 @@ function wingsOf(a: Animal): number { console.log(wingsOf(new Bird()), wingsOf(new Animal())); console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(",")); console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime()); + +// A type assertion of the global object denotes the global object itself. +const COUNT = Symbol.for("no-divergences.count"); +const globalState = globalThis as { [COUNT]?: { n: number } }; +globalState[COUNT] = { n: 1 }; +console.log(globalState[COUNT]?.n); diff --git a/tests/coverage-fixtures/tier-demotions.ts b/tests/coverage-fixtures/tier-demotions.ts index 52f2058f1..3dbd0c3fe 100644 --- a/tests/coverage-fixtures/tier-demotions.ts +++ b/tests/coverage-fixtures/tier-demotions.ts @@ -17,9 +17,9 @@ function readName(text: string): string { return String(data.name); } -// An operation without a static lowering, on a parameter. +// An operation without a static lowering (the host's default locale), on a parameter. function precise(n: number): string { - return n.toPrecision(3); + return n.toLocaleString(); } console.log(readName('{"name":"a"}'), precise(1.5), new Point(1, 2).x); diff --git a/tests/diagnostics/builtin-class-alias-escape.ts b/tests/diagnostics/builtin-class-alias-escape.ts index b3f9be5d3..29186738c 100644 --- a/tests/diagnostics/builtin-class-alias-escape.ts +++ b/tests/diagnostics/builtin-class-alias-escape.ts @@ -1,15 +1,7 @@ -// A builtin class alias owns no runtime value in a TypeScript file: +// A Map or Set alias owns no runtime value in a TypeScript file: // construction, instanceof, typeof and .name compile, other uses report -// at the use. A let binding of a class is not an alias. -import { EventEmitter } from "node:events"; - -const Emitter = EventEmitter; -const kinds = [Emitter]; -console.log(kinds.length); - +// at the use. Aliases of EventEmitter, the stream classes and the error +// classes are class values. const M = Map; const boxed: unknown = M; console.log(boxed !== undefined); - -let Mutable = EventEmitter; -console.log(typeof Mutable); diff --git a/tests/diagnostics/class-values.ts b/tests/diagnostics/class-values.ts index 3a53e5185..4e5a6ea4c 100644 --- a/tests/diagnostics/class-values.ts +++ b/tests/diagnostics/class-values.ts @@ -18,9 +18,9 @@ class Unrelated { static tag = "unrelated"; } -// Builtin classes as values: a const alias resolves at compile time, and -// an array of builtin classes has no element type. -const E = Error; +// Builtin classes with their own construction as values (Error and its +// standard subclasses are values; AggregateError is not). +const E = AggregateError; const errorClasses = [E]; // Classes extending builtin bases as values (the error-message completion diff --git a/tests/diagnostics/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/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index b79836920..e42f8a07a 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -48,8 +48,8 @@ function fromUnion(u: E1 | E2): string { console.log(fromUnion({ kind: "one", b: "s", count: 1 })); const xs = [1, 2, 3]; -const { length: len } = xs; // object patterns over arrays keep the fence -console.log(len); +const { push: append } = xs; // object patterns over arrays read positions and length only +console.log(typeof append); const indexed: Record = { a: 1 }; const { ...allOfIt } = indexed; // index-signature rest needs overflow packing diff --git a/tests/diagnostics/dynamic-surface.ts b/tests/diagnostics/dynamic-surface.ts index 537a992eb..3af6671f4 100644 --- a/tests/diagnostics/dynamic-surface.ts +++ b/tests/diagnostics/dynamic-surface.ts @@ -4,11 +4,12 @@ // flag — never an ICE, never a link error. Static Math methods include // the scalar functions and variadic Math.hypot. The .split(string), // trim/pad variants, parseInt, isNaN, the global parseFloat/isFinite and -// Number.parseFloat/Number.parseInt over exactly-typed arguments, and -// string-pattern replacement compile statically now and no longer appear -// here.) +// Number.parseFloat/Number.parseInt over exactly-typed arguments, +// string-pattern replacement and toPrecision compile statically now and no +// longer appear here; toLocaleString without a locale still has no +// lowering.) const up = Math.cbrt(2); const tau = Math.PI * 2; -const price = (19.99).toPrecision(4); +const price = (19.99).toLocaleString(); const n = Number.parseFloat("3.14"); void n; 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/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts new file mode 100644 index 000000000..d162872fb --- /dev/null +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -0,0 +1,18 @@ +// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, +// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' +// key format and crypto.subtle or its methods taken as values are refused +// by name; the bundled declarations omit the other SubtleCrypto methods +// (node-types/fenced.ts pins their fence under @types/node). +const fromJwk = await crypto.subtle.importKey( + "jwk", + { kty: "oct", k: "c2VjcmV0" }, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(fromJwk.type); + +const subtle = crypto.subtle; +console.log(typeof subtle); + +console.log(typeof crypto.subtle.sign); 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-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index b884de3ce..78fbf2af7 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -97,4 +97,15 @@ void callbackified; * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); cipherStream.on("data", () => {}); +/* WebCrypto beyond the HMAC slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside importKey, sign, verify + * and digest fence by name. */ +async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +} +void encryptWithWebCrypto; +/* Node's navigator global: typeof navigator and navigator.userAgent + * compile; the other members fence by name. */ +const cores = navigator.hardwareConcurrency; +void cores; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/navigator.ts b/tests/fixtures/node-types/navigator.ts new file mode 100644 index 000000000..2f1ee2d9f --- /dev/null +++ b/tests/fixtures/node-types/navigator.ts @@ -0,0 +1,6 @@ +/* Node 24's navigator global under @types/node: the runtime probe + * libraries use (jose's user-agent check) folds to Node's values. */ +const browser = !navigator.userAgent.startsWith("Mozilla/5.0 "); +console.log(typeof navigator, navigator.userAgent, browser); +console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"); +console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length); diff --git a/tests/fixtures/node-types/webcrypto-hmac.ts b/tests/fixtures/node-types/webcrypto-hmac.ts new file mode 100644 index 000000000..4b37d7de4 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-hmac.ts @@ -0,0 +1,25 @@ +/* WebCrypto's HMAC slice under @types/node: the crypto global's subtle + * methods and CryptoKey members lower to the same native values as under + * the bundled declarations. @types/node names the dictionary and key + * types in node:crypto's webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const encoder = new TextEncoder(); + const params: webcrypto.HmacImportParams = { name: "HMAC", hash: "SHA-512" }; + const key: webcrypto.CryptoKey = await crypto.subtle.importKey("raw", encoder.encode("node-types"), params, true, [ + "verify", + "sign", + ]); + const algorithm = key.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(key.type, key.extractable, key.usages.join(","), algorithm.name, algorithm.length, algorithm.hash.name); + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode("message")); + console.log(signature.byteLength, Buffer.from(signature).toString("base64")); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("message"))); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("massage"))); + const digest = await crypto.subtle.digest("SHA-384", encoder.encode("message")); + console.log(Buffer.from(digest).toString("hex")); + console.log(key instanceof CryptoKey); +} + +void main(); 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..da0a3dc52 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; } } @@ -50,8 +50,8 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getBaseTypeOfLiteralType(answerType) === number, "number literal base"); check(facade.getTypeOfSymbol(answerSymbol) === answerType, "symbol type"); check(facade.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location type"); - check(facade.declarationsOf(answerSymbol)[0] === answer, "declaration identity"); - check(facade.valueDeclarationOf(answerSymbol) === answer, "value declaration"); + check(answerSymbol.declarations[0] === answer, "declaration identity"); + check(answerSymbol.valueDeclaration === answer, "value declaration"); check(facade.getContextualType(answerName) === undefined, "absent contextual type"); check(facade.getSymbolAtLocation(root.endOfFileToken) === undefined, "absent symbol"); check(facade.getUnknownType().flags === TypeFlags.Unknown, "unknown intrinsic"); @@ -77,22 +77,22 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque const identityType = facade.getTypeAtLocation(identity.name!); const signature = facade.getSignatureFromDeclaration(identity)!; check(facade.getCallSignatures(identityType)[0] === signature, "call signature identity"); - check(facade.signatureDeclaration(signature) === identity, "signature declaration identity"); + check(signature.declaration === identity, "signature declaration identity"); check(facade.typeToString(facade.getReturnTypeOfSignature(signature)) === "T", "generic return type"); check(facade.getTypePredicateOfSignature(signature) === undefined, "absent predicate"); const isBox = facade.getSignatureFromDeclaration(declaration(root, "isBox"))!; check(facade.getTypePredicateOfSignature(isBox)!.type === boxType, "predicate type identity"); const call = declaration(root, "called").initializer!; const resolved = facade.getResolvedSignature(call)!; - check(facade.signatureDeclaration(resolved) === identity, "instantiated signature declaration"); + check(resolved.declaration === 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!; @@ -140,11 +140,11 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getTypeOfSymbol(answerSymbol) === answerType, "warm symbol type"); check(facade.getDeclaredTypeOfSymbol(boxSymbol) === boxType, "warm declared type"); check(facade.getAliasedSymbol(alias) === answerSymbol, "warm alias"); - check(facade.declarationsOf(answerSymbol)[0] === answer, "warm declarations"); - check(facade.valueDeclarationOf(answerSymbol) === answer, "warm value declaration"); + check(answerSymbol.declarations[0] === answer, "warm declarations"); + check(answerSymbol.valueDeclaration === answer, "warm value declaration"); check(facade.getResolvedSignature(call) === resolved, "warm resolved signature"); check(facade.getSignatureFromDeclaration(identity) === signature, "warm signature"); - check(facade.signatureDeclaration(signature) === identity, "warm signature declaration"); + check(signature.declaration === identity, "warm signature declaration"); check(facade.getTypePredicateOfSignature(signature) === undefined, "warm absent predicate"); check(facade.getTypePredicateOfSignature(isBox)!.type === boxType, "warm predicate"); check(facade.getCallSignatures(identityType)[0] === signature, "warm call signatures"); @@ -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..710255433 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. @@ -60,7 +60,7 @@ export function checkCheckerSnapshots(session: Ts7Session, client: Ts7RpcClient, check(oldArms.length === 2 && siblingArms.length === 2, "original constituents"); check(oldArms !== siblingArms && oldArms[0] !== siblingArms[0], "project-local constituents"); const oldSymbol = old.getSymbolAtLocation(name)!; - const oldDeclaration = old.valueDeclarationOf(oldSymbol)!; + const oldDeclaration = oldSymbol.valueDeclaration!; check(oldDeclaration.name === name, "resolved declaration identity"); value = 'export const value: string | null = "new";'; @@ -73,7 +73,7 @@ export function checkCheckerSnapshots(session: Ts7Session, client: Ts7RpcClient, const newSymbol = next.getSymbolAtLocation(name)!; check(next.typeToString(newType) === "string | null", "new snapshot result"); check(newType !== oldType && newSymbol !== oldSymbol, "independent semantic identities"); - check(next.valueDeclarationOf(newSymbol) === oldDeclaration, "declarations retain shared syntax identity"); + check(newSymbol.valueDeclaration === oldDeclaration, "declarations retain shared syntax identity"); check(next.getTypeOfSymbol(newSymbol) === newType, "new symbol answer"); check(old.getTypeOfSymbol(oldSymbol) === oldType, "old symbol answer"); check(old.getTypeAtLocation(name) === oldType && old.typeToString(oldType) === "number | null", "old snapshot stays immutable"); diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index 2aba713c3..d97f90892 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)!; @@ -267,7 +267,7 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st const typeText = checker.typeToString(ownedType); check(checker.getTypeAtPosition(file, content.indexOf("answer")) === ownedType, "semantic type identity"); check(checker.getSymbolAtPosition(file, content.indexOf("answer")) === ownedSymbol, "semantic symbol identity"); - check(ownedSymbol.declarations[0]!.resolve() === declaration, "semantic declaration identity"); + check(ownedSymbol.declarationHandles[0]!.resolve() === declaration, "semantic declaration identity"); check(typeText === "42" && ownedType.isNumberLiteralType() && ownedType.value === 42, "semantic literal metadata"); // The semantic exercise disposes its graph; the session still owns the // server snapshot and its program until explicit snapshot disposal. 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..41b61b824 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; } } @@ -81,8 +81,8 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(checker.getTypeOfSymbol(answerSymbol) === answerType, "symbol type identity"); check(checker.getTypeOfSymbol([answerSymbol, answerSymbol])[1] === answerType, "batch symbol types"); check(checker.getTypeOfSymbolAtLocation(answerSymbol, answerName) === answerType, "location-dependent symbol type"); - check(answerSymbol.valueDeclaration?.resolve() === answer && answerSymbol.declarations[0]!.resolve() === answer, "default declaration resolution"); - check(answerSymbol.declarations[0]!.resolve(checker.project) === answer, "explicit declaration project"); + check(answerSymbol.valueDeclarationHandle?.resolve() === answer && answerSymbol.declarationHandles[0]!.resolve() === answer, "default declaration resolution"); + check(answerSymbol.declarationHandles[0]!.resolve(checker.project) === answer, "explicit declaration project"); check(checker.getTypeAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerType, "position type batch"); check(checker.getSymbolAtPosition(root.fileName, [answerName.getStart(), answerName.getStart()])[1] === answerSymbol, "position symbol batch"); check(checker.getResolvedSymbol(answerName)!.getExportSymbol() === answerSymbol, "resolved local/export symbol identity"); @@ -123,7 +123,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic // the binder's member symbol is distinct but refers to the same AST node. const boundMember = boxSymbol.getMembers().get("value")!; const checkedMember = checker.getPropertyOfType(boxType, "value")!; - check(boundMember !== checkedMember && boundMember.declarations[0]!.resolve() === checkedMember.declarations[0]!.resolve(), "bound and instantiated member declarations"); + check(boundMember !== checkedMember && boundMember.declarationHandles[0]!.resolve() === checkedMember.declarationHandles[0]!.resolve(), "bound and instantiated member declarations"); check(boxSymbol.getMembers() === boxSymbol.getMembers() && boxSymbol.getExports() === boxSymbol.getExports(), "symbol table cache"); check(boxType.getBaseTypes()!.length === 0 && checker.getBaseTypes(boxType).length === 0, "empty base types"); const child = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Child").name!)); @@ -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,8 +147,8 @@ 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.declaration!.resolve() === identity, "signature declaration"); + check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); + check(signature.declarationHandle!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); const constraint = checker.getConstraintOfTypeParameter(signatureTypes[0]!)!; @@ -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()!; + const exportSpecifier = renamed.declarationHandles[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"); @@ -235,7 +235,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic try { answerType.getRegularType(); } catch { refused = true; } check(refused, "type access after disposal"); refused = false; - try { answerSymbol.declarations[0]!.resolve(); } catch { refused = true; } + try { answerSymbol.declarationHandles[0]!.resolve(); } catch { refused = true; } check(refused, "declaration access after disposal"); } 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__/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__/coverage-dynamic-mix.txt b/tests/harness/__snapshots__/coverage-dynamic-mix.txt index 2ad84f0af..0bff0f03c 100644 --- a/tests/harness/__snapshots__/coverage-dynamic-mix.txt +++ b/tests/harness/__snapshots__/coverage-dynamic-mix.txt @@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them) blockers: - ×1 SC2012 '.toPrecision()' on numbers cannot compile statically + ×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet at tests/coverage-fixtures/dynamic-mix.ts:6:12 - hint: use a supported API or rewrite this operation for native compilation \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-explain-dynamic.txt b/tests/harness/__snapshots__/coverage-explain-dynamic.txt index 1c86a808c..f168988e6 100644 --- a/tests/harness/__snapshots__/coverage-explain-dynamic.txt +++ b/tests/harness/__snapshots__/coverage-explain-dynamic.txt @@ -12,5 +12,5 @@ function tests/coverage-fixtures/tier-demotions.ts:1:1 (1 site inside) function precise tests/coverage-fixtures/tier-demotions.ts:21:1 (1 site inside) binding n in precise tests/coverage-fixtures/tier-demotions.ts:21:18 (1 request) - <- SC2012 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: '.toPrecision()' on numbers cannot compile statically + <- SC2020 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet -> function precise turns dynamic \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-mixed.txt b/tests/harness/__snapshots__/coverage-mixed.txt index c3b348f9c..cf78aa9e0 100644 --- a/tests/harness/__snapshots__/coverage-mixed.txt +++ b/tests/harness/__snapshots__/coverage-mixed.txt @@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/mixed.ts in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) blockers: - ×1 SC1031 object destructuring of non-record values (the source is array-typed) + ×1 SC1031 object destructuring of non-record values (the source is union-typed) at tests/coverage-fixtures/mixed.ts:13:7 hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations) \ No newline at end of file diff --git a/tests/harness/__snapshots__/coverage-tier-demotions.txt b/tests/harness/__snapshots__/coverage-tier-demotions.txt index 7b52f9d63..aaf1f4e3b 100644 --- a/tests/harness/__snapshots__/coverage-tier-demotions.txt +++ b/tests/harness/__snapshots__/coverage-tier-demotions.txt @@ -8,6 +8,6 @@ scriptc coverage tests/coverage-fixtures/tier-demotions.ts ×1 SC1090 assignment to non-variables at tests/coverage-fixtures/tier-demotions.ts:10:1 hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary) - ×1 SC2012 '.toPrecision()' on numbers cannot compile statically + ×1 SC2020 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet at tests/coverage-fixtures/tier-demotions.ts:22:10 - hint: use a supported API or rewrite this operation for native compilation \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ 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 90928dae7..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(), toUTCString(), 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(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering \ No newline at end of file + 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__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 65d9c293b..743bb5657 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -38,9 +38,9 @@ destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on destructuring-fences.ts:51:7 - error SC1031: object destructuring of non-record values (the source is array-typed) are not supported yet 50 | const xs = [1, 2, 3]; - 51 | const { length: len } = xs; // object patterns over arrays keep the fence - | ^~~~~~~~~~~~~~~ - 52 | console.log(len); + 51 | const { push: append } = xs; // object patterns over arrays read positions and length only + | ^~~~~~~~~~~~~~~~ + 52 | console.log(typeof append); destructuring-fences.ts:55:9 - error SC1031: rest bindings over index-signature shapes (the undeclared entries would need overflow packing) are not supported yet diff --git a/tests/harness/__snapshots__/dynamic-surface.ts.txt b/tests/harness/__snapshots__/dynamic-surface.ts.txt index 2744d29e5..d4fe2ac7e 100644 --- a/tests/harness/__snapshots__/dynamic-surface.ts.txt +++ b/tests/harness/__snapshots__/dynamic-surface.ts.txt @@ -1,8 +1,8 @@ -dynamic-surface.ts:12:15 - error SC2012: '.toPrecision()' on numbers cannot compile statically +dynamic-surface.ts:13:15 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet - 11 | const tau = Math.PI * 2; - 12 | const price = (19.99).toPrecision(4); - | ^~~~~~~~~~~~~~~~~~~~~~ - 13 | const n = Number.parseFloat("3.14"); + 12 | const tau = Math.PI * 2; + 13 | const price = (19.99).toLocaleString(); + | ^~~~~~~~~~~~~~~~~~~~~~~~ + 14 | const n = Number.parseFloat("3.14"); - hint: use a supported API or rewrite this operation for native compilation \ No newline at end of file + hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US") \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 29e4a2ee9..81db57e65 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -210,6 +210,24 @@ fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); 99 | cipherStream.on("data", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | // End of the declared-but-not-lowered surface. + 100 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not \ No newline at end of file + hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not + +fenced.ts:104:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet + + 103 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 104 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + | ^~~~~~~~~~~~~~~~~~~~~ + 105 | } + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +fenced.ts:109:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet + + 108 | * compile; the other members fence by name. */ + 109 | const cores = navigator.hardwareConcurrency; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 110 | void cores; + + hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt b/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt deleted file mode 100644 index dfd5f0eaa..000000000 --- a/tests/harness/__snapshots__/retired-engine-1112-number-methods.ts.txt +++ /dev/null @@ -1,8 +0,0 @@ -retired-engine-1112-number-methods.ts:9:13 - error SC2012: '.toPrecision()' on numbers cannot compile statically - - 8 | console.log((1e21).toFixed(2), (0 / 0).toFixed(2)); - 9 | console.log(n.toPrecision(2), n.toPrecision(8), (0.000123).toPrecision(2)); - | ^~~~~~~~~~~~~~~~ - 10 | console.log((255).toString(16), (255).toString(2), (511).toString(8), (12345).toString(36)); - - hint: use a supported API or rewrite this operation for native compilation \ No newline at end of file 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 95fbe1fff..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(), toUTCString(), 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__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt new file mode 100644 index 000000000..521edff3c --- /dev/null +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -0,0 +1,26 @@ +webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet + + 6 | const fromJwk = await crypto.subtle.importKey( + 7 | "jwk", + | ^~~~~ + 8 | { kty: "oct", k: "c2VjcmV0" }, + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet + + 14 | + 15 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 16 | console.log(typeof subtle); + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet + + 17 | + 18 | console.log(typeof crypto.subtle.sign); + | ^~~~~~~~~~~~~~~~~~ + 19 | + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 98055a9f1..f11150ee9 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2092,6 +2092,7 @@ "5812-dyn-box-async-fields.ts": 3522, "5813-mock-timers-module.ts": 3766, "5814-mock-timers-date.ts": 3760, + "5815-als-callback-context.ts": 3110, "5830-timers-promises-signal.ts": 3875, "5831-util-types-brands.ts": 3347, "5832-util-types-brands-dynamic.js": 3250, @@ -2202,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, @@ -2213,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, @@ -2223,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, @@ -2239,6 +2248,14 @@ "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, + "6077-generator-helpers-untyped.js": 2550, + "6078-destructure-untyped-iterables.js": 2500, + "6079-generator-helpers-any.ts": 2550, + "6080-take-then-flatmap.ts": 2600, "6100-record-get-or-create.ts": 3771, "6101-record-get-or-create-unchecked/main.ts": 2107, "6103-typeof-builtin-bindings.cjs": 2202, @@ -2259,6 +2276,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, @@ -5167,6 +5185,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, @@ -5277,6 +5296,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, @@ -5288,6 +5309,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, @@ -5298,7 +5321,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, @@ -5314,6 +5341,14 @@ "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, + "6077-generator-helpers-untyped.js": 3050, + "6078-destructure-untyped-iterables.js": 2550, + "6079-generator-helpers-any.ts": 2700, + "6080-take-then-flatmap.ts": 2750, "6100-record-get-or-create.ts": 3173, "6101-record-get-or-create-unchecked/main.ts": 3228, "6103-typeof-builtin-bindings.cjs": 1796, @@ -5334,6 +5369,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/corpus-costs/1112-number-methods.ts.json b/tests/harness/corpus-costs/1112-number-methods.ts.json new file mode 100644 index 000000000..b99785c0f --- /dev/null +++ b/tests/harness/corpus-costs/1112-number-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 3859, "san": 43669 } diff --git a/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json b/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json new file mode 100644 index 000000000..4203ad979 --- /dev/null +++ b/tests/harness/corpus-costs/5639-union-arm-by-discriminant.ts.json @@ -0,0 +1 @@ +{ "plain": 1800, "san": 1900 } diff --git a/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json b/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json new file mode 100644 index 000000000..0b3994fcd --- /dev/null +++ b/tests/harness/corpus-costs/5640-generic-structural-class-narrowing.ts.json @@ -0,0 +1 @@ +{ "plain": 2300, "san": 3300 } diff --git a/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json b/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json new file mode 100644 index 000000000..2d2f07e08 --- /dev/null +++ b/tests/harness/corpus-costs/6023-string-compare-utf16.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 5400 } diff --git a/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json b/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json new file mode 100644 index 000000000..645cfe9a7 --- /dev/null +++ b/tests/harness/corpus-costs/6200-string-annexb-locale-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2795, "san": 4147 } diff --git a/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json b/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json new file mode 100644 index 000000000..98160b1ea --- /dev/null +++ b/tests/harness/corpus-costs/6201-number-precision-exponential.ts.json @@ -0,0 +1 @@ +{ "plain": 1595, "san": 2653 } diff --git a/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json b/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json new file mode 100644 index 000000000..d4cc558f1 --- /dev/null +++ b/tests/harness/corpus-costs/6202-dyn-string-number-methods.js.json @@ -0,0 +1 @@ +{ "plain": 4232, "san": 4434 } diff --git a/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json b/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json new file mode 100644 index 000000000..27c124839 --- /dev/null +++ b/tests/harness/corpus-costs/6203-any-string-number-methods.ts.json @@ -0,0 +1 @@ +{ "plain": 2843, "san": 5904 } diff --git a/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json b/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json new file mode 100644 index 000000000..ecb1d73b4 --- /dev/null +++ b/tests/harness/corpus-costs/6204-crypto-get-random-values.ts.json @@ -0,0 +1 @@ +{ "plain": 3066, "san": 3476 } diff --git a/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json b/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json new file mode 100644 index 000000000..dff4ea74a --- /dev/null +++ b/tests/harness/corpus-costs/6205-crypto-get-random-values.js.json @@ -0,0 +1 @@ +{ "plain": 2527, "san": 3675 } diff --git a/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json b/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json new file mode 100644 index 000000000..80af3c965 --- /dev/null +++ b/tests/harness/corpus-costs/6206-buffer-is-buffer-views.ts.json @@ -0,0 +1 @@ +{ "plain": 3062, "san": 3821 } diff --git a/tests/harness/corpus-costs/6207-fs-constants-object.ts.json b/tests/harness/corpus-costs/6207-fs-constants-object.ts.json new file mode 100644 index 000000000..9a68cf16c --- /dev/null +++ b/tests/harness/corpus-costs/6207-fs-constants-object.ts.json @@ -0,0 +1 @@ +{ "plain": 2367, "san": 1937 } diff --git a/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json b/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json new file mode 100644 index 000000000..377f8328c --- /dev/null +++ b/tests/harness/corpus-costs/6208-fs-constants-require.cjs.json @@ -0,0 +1 @@ +{ "plain": 3082, "san": 3887 } diff --git a/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json b/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json new file mode 100644 index 000000000..db5f0ebc8 --- /dev/null +++ b/tests/harness/corpus-costs/6209-buffer-module-alias.cjs.json @@ -0,0 +1 @@ +{ "plain": 2310, "san": 4242 } diff --git a/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json b/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json new file mode 100644 index 000000000..ccf3a0f37 --- /dev/null +++ b/tests/harness/corpus-costs/6210-typeof-global-static-members.js.json @@ -0,0 +1 @@ +{ "plain": 3244, "san": 5476 } diff --git a/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..c6ea1665b --- /dev/null +++ b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json @@ -0,0 +1 @@ +{ "plain": 20300, "san": 40800 } diff --git a/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/README.md b/tests/harness/corpus-costs/README.md new file mode 100644 index 000000000..e005fa793 --- /dev/null +++ b/tests/harness/corpus-costs/README.md @@ -0,0 +1,9 @@ +# Per-program corpus costs + +A change that adds a corpus program, or changes how long one takes, records the program's CI cost here instead of editing `../corpus-costs.json`: one file per program, named after it, with the milliseconds of its differential plus llvm-differential case per flavor. + +```json +{ "plain": 2700, "san": 3200 } +``` + +`5637-child-max-buffer-sync.ts` goes in `5637-child-max-buffer-sync.ts.json`, and a directory program such as `2946-child-fork-ipc/main.ts` in `2946-child-fork-ipc/main.ts.json`. Either flavor may be left out. These files override the table for shard balancing (`tests/harness/shard.ts`), and `scripts/ci-corpus-costs.mjs` folds them into the table and deletes them when it refreshes the costs from CI logs. Separate files keep changes that add programs from editing the same lines of the table. diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index f1347bf23..8cf5d6719 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -295,7 +295,7 @@ test("--explain-dynamic attributes each refusal to the unit the dynamic tier wou requests.map((r) => [r.code, r.seed, r.scope, r.unit.kind, r.unit.name, r.enclosing.name]), ).toEqual([ ["SC1090", "reflection", "reached", "class", "Point", ""], - ["SC2012", "operation", "reached", "binding", "n", "precise"], + ["SC2020", "operation", "reached", "binding", "n", "precise"], ]); expect(tiers.units.map((u) => [u.kind, u.name, u.requests, u.sites])).toEqual([ ["class", "Point", 1, 0], diff --git a/tests/harness/differential.test.ts b/tests/harness/differential.test.ts index a2206fd97..d8139299d 100644 --- a/tests/harness/differential.test.ts +++ b/tests/harness/differential.test.ts @@ -36,7 +36,7 @@ const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); // SCRIPTC_TEST_SHARD="i/n" (CI's matrix) keeps only this shard's slice of // the corpus — the file is this suite's wall-time monster, so vitest's // file-granular --shard alone cannot split it. Slices are balanced by each -// program's measured cost (corpus-costs.json). Unset = everything. +// program's measured cost (corpus-costs.json and corpus-costs/). Unset = everything. const ENTRY_EXTS = ["ts", "js", "mjs", "cjs"]; const files = corpusSelect( ENTRY_EXTS.flatMap((ext) => [ diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 92ac8f5ab..03d533d62 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1130,3 +1130,65 @@ console.log(count(require("./lib.cjs"))); ); }); }); + +// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and +// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice +// reject with a coded SC2020 Error instead of a Node-shaped success, and a +// JavaScript call with the 'jwk' format meets its named fence when it runs. +// Node's own rejections (unknown names, bad usages) are corpus-pinned +// (6292, 6294); these refusals have no Node twin, so they live here. +describe("WebCrypto refusals (JS lane)", () => { + test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + const r = await compileAndRun( + "webcrypto-unsupported-algorithms", + `const bytes = new TextEncoder().encode("secret"); +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("rsa", () => + crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]), +); +const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); +await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); +await attempt("sha3 hmac", () => + crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), +); +const format = JSON.parse('"jwk"'); +await attempt("computed jwk", () => + crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +`, + "mjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" + + "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + + "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + ); + }); + + test("a literal 'jwk' import is a named fence where it runs", async () => { + const r = await compileAndRun( + "webcrypto-jwk-literal", + `console.log("before"); +await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +`, + "mjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("before\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + ); + }); +}); diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts index 0eb0cf2c3..ffd42bd8e 100644 --- a/tests/harness/library-mode.test.ts +++ b/tests/harness/library-mode.test.ts @@ -1053,12 +1053,13 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { test("a manifest-id-keyed teachings entry attaches to that surface's own refusal", async () => { const diags = await refusal( - `export function f(): number { return (1.234).toPrecision(2).length; }\n`, + `import { brotliCompressSync } from "node:zlib";\nexport function f(): number { return brotliCompressSync("data").length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { teachings: { - "stdlib.number.toPrecision": "formatting runs in the host; request it as an effect", + "node-builtin.zlib.brotliCompressSync": + "compression runs in the host; request it as an effect", }, }, }, @@ -1066,27 +1067,29 @@ describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { ); // The surface's own code, not a fence code: the id key attaches text // to the refusal that already fires. - expect(diags[0]!.code).toBe("SC2012"); - expect(diags[0]!.message).toContain(".toPrecision()"); + expect(diags[0]!.code).toBe("SC2020"); + expect(diags[0]!.message).toContain("zlib.brotliCompressSync"); expect(diags[0]!.note).toBe( - "from the 'refusal-fixture' profile: formatting runs in the host; request it as an effect", + "from the 'refusal-fixture' profile: compression runs in the host; request it as an effect", ); }); test("fencing a surface the static tier refuses anyway changes only the message", async () => { const diags = await refusal( - `export function f(): number { return (1.234).toPrecision(2).length; }\n`, + `import { brotliCompressSync } from "node:zlib";\nexport function f(): number { return brotliCompressSync("data").length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { - fences: [{ id: "stdlib.number.toPrecision", teaching: "formatting is host work" }], + fences: [ + { id: "node-builtin.zlib.brotliCompressSync", teaching: "compression is host work" }, + ], }, }, emission, ); // The existing refusal's code survives — the fence never re-codes a // surface that already refuses; its teaching rides as the note. - expect(diags[0]!.code).toBe("SC2012"); - expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: formatting is host work"); + expect(diags[0]!.code).toBe("SC2020"); + expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: compression is host work"); }); }); diff --git a/tests/harness/library-profile.test.ts b/tests/harness/library-profile.test.ts index caa3b4e72..eb1bcc4cd 100644 --- a/tests/harness/library-profile.test.ts +++ b/tests/harness/library-profile.test.ts @@ -481,7 +481,7 @@ describe("library profile fences", () => { determinism: { remediations: { SC2012: "the explicit map key wins" }, fences: [ - { id: "stdlib.number.toPrecision", remediation: "request it as an effect" }, + { id: "diagnostic.sc2012", remediation: "request it as an effect" }, { id: "node-builtin.crypto.createSign", remediation: "signatures come from the host" }, ], }, @@ -551,7 +551,10 @@ describe("library profile fences", () => { "stdlib.date.getUTCSeconds", "stdlib.date.now", "stdlib.date.parse", + "stdlib.date.toDateString", "stdlib.date.toISOString", + "stdlib.date.toString", + "stdlib.date.toTimeString", "stdlib.date.toUTCString", "stdlib.date.valueOf", ]); diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts index 475db54aa..288d15481 100644 --- a/tests/harness/native-frontend.ts +++ b/tests/harness/native-frontend.ts @@ -66,6 +66,9 @@ export const programs = [ "class-array-optional-return.ts", "contextual-array-union-write.ts", "6020-class-extends-function-constructor.js", + // Re-chosen key orders rebuild the inspect helpers after lowering. + "key-order-late-optional.ts", + "5888-inspect-sorted.ts", ]; interface LowerRequest { diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index b6af0dbb6..54207f246 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -52,7 +52,7 @@ const KNOWN: Record = { "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, "fetch-destructure": { run: "SC1031" }, - "fs-shims": { run: "SC2020" }, + "fs-shims": { run: "SC1101" }, "hoisted-workspace": { build: "SC2013" }, "import-refusal": { build: "SC2020" }, "island-registry": { build: "SC2011" }, diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index bf57a830a..802fbd3e1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -839,6 +839,42 @@ test("node-types: Cipheriv and Decipheriv match Node under @types/node", async ( expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-hmac"); + const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-hmac"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + +test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { + const outDir = outDirFor("node-navigator"); + const entry = join(nodeTypesDir, "navigator.ts"); + const result = await compile(entry, { + outPath: join(outDir, "navigator"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: declared-but-not-lowered surface fences, naming @types/node", async () => { const outDir = outDirFor("node-fenced"); const result = await compile(join(nodeTypesDir, "fenced.ts"), { 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/shard.test.ts b/tests/harness/shard.test.ts index e645f2cac..61eb84083 100644 --- a/tests/harness/shard.test.ts +++ b/tests/harness/shard.test.ts @@ -3,19 +3,20 @@ * depends only on the item's key (list growth never moves an existing item), * and no spec means everything. differential/llvm-differential/npm/server * lean on these properties for CI sharding — see shard.ts. */ -import { globSync } from "node:fs"; +import { globSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { balancedShardSelect, + corpusCostFiles, corpusSelect, + loadCorpusCosts, parseShardSpec, shardOf, shardSelect, shardSuffix, skipUnownedTest, } from "./shard.js"; -import corpusCosts from "./corpus-costs.json"; import fixtureCosts from "./effect4-costs.json"; import effectCosts from "./effect-costs.json"; @@ -268,7 +269,7 @@ describe("corpusSelect", () => { ); test("measured costs cover the corpus and balance the shards", () => { - const costs = corpusCosts.plain as Record; + const costs = loadCorpusCosts().plain; const measured = programs.filter((k) => costs[k] !== undefined).length; expect(measured / programs.length).toBeGreaterThan(0.9); const loads = Array.from({ length: 10 }, (_, i) => @@ -279,4 +280,30 @@ describe("corpusSelect", () => { ); expect(Math.max(...loads) / Math.min(...loads)).toBeLessThan(1.1); }); + + test("every per-program cost file names a corpus program", () => { + const known = new Set(programs); + for (const key of corpusCostFiles().keys()) + expect(known, `corpus-costs/${key}.json`).toContain(key); + }); + + test("per-program cost files override the table, and invalid ones throw", () => { + const dir = join( + import.meta.dirname, + "../../node_modules/.cache/scriptc-tests/corpus-costs-files", + ); + rmSync(dir, { recursive: true, force: true }); + mkdirSync(join(dir, "200-dir"), { recursive: true }); + writeFileSync(join(dir, "100-flat.ts.json"), '{ "plain": 2500, "san": 4000 }\n'); + writeFileSync(join(dir, "200-dir/main.ts.json"), '{ "san": 900 }\n'); + expect([...corpusCostFiles(dir)]).toEqual([ + ["100-flat.ts", { plain: 2500, san: 4000 }], + ["200-dir/main.ts", { san: 900 }], + ]); + for (const bad of ['{ "plian": 1 }', '{ "plain": 0 }', '{ "san": "9" }']) { + writeFileSync(join(dir, "100-flat.ts.json"), bad); + expect(() => corpusCostFiles(dir)).toThrow(/plain|san/); + } + rmSync(dir, { recursive: true, force: true }); + }); }); diff --git a/tests/harness/shard.ts b/tests/harness/shard.ts index 4c3b9c244..a2797acf9 100644 --- a/tests/harness/shard.ts +++ b/tests/harness/shard.ts @@ -16,7 +16,9 @@ * everything: local runs are unchanged. */ import { createHash } from "node:crypto"; -import { readFileSync } from "node:fs"; +import { globSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import type { Suite, Test, TestContext } from "vitest"; export interface ShardSpec { @@ -94,11 +96,53 @@ export function balancedShardSelect( return items.filter((_, index) => owners[index] === spec.index); } -/** Measured milliseconds per corpus program and flavor (corpus-costs.json): its differential plus - * llvm-differential case on a CI runner. */ -type CorpusCosts = { plain: Record; san: Record }; +/** Measured milliseconds per corpus program and flavor: its differential plus llvm-differential case on a CI + * runner. corpus-costs.json is the table scripts/ci-corpus-costs.mjs refreshes from CI logs. A change that adds + * or remeasures a program records its costs in corpus-costs/.json instead (`{ "plain": ms, "san": ms }`, + * a directory program as corpus-costs//main.ts.json), which overrides the table until the next refresh + * folds it in. One file per program keeps changes that add programs from editing the same lines. */ +export type CorpusCosts = { plain: Record; san: Record }; let corpusCostTable: CorpusCosts | undefined; +/** The per-program cost files: program key → its file's costs. Throws on a field other than plain/san or a + * cost that is not a positive finite number. */ +export function corpusCostFiles( + dir: string = fileURLToPath(new URL("./corpus-costs", import.meta.url)), +): Map>> { + const files = new Map>>(); + for (const rel of globSync("**/*.json", { cwd: dir }) + .map((path) => path.replaceAll("\\", "/")) + .sort()) { + const key = rel.slice(0, -".json".length); + const entry = JSON.parse(readFileSync(join(dir, rel), "utf8")) as Record; + for (const [flavor, cost] of Object.entries(entry)) { + if ( + (flavor !== "plain" && flavor !== "san") || + typeof cost !== "number" || + !Number.isFinite(cost) || + cost <= 0 + ) + throw new Error( + `corpus-costs/${rel}: ${flavor} must be "plain" or "san" with a positive cost`, + ); + } + files.set(key, entry as Partial>); + } + return files; +} + +/** corpus-costs.json with the per-program files applied. */ +export function loadCorpusCosts(): CorpusCosts { + const table = JSON.parse( + readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"), + ) as CorpusCosts; + for (const [key, entry] of corpusCostFiles()) { + if (entry.plain !== undefined) table.plain[key] = entry.plain; + if (entry.san !== undefined) table.san[key] = entry.san; + } + return table; +} + /** The corpus programs this shard owns, balanced by measured cost. differential.test.ts and * llvm-differential.test.ts both select with this, so a shard owns the same programs in both lanes and their * compile cache serves both. A program without a measurement weighs as much as the median measured one, so @@ -109,9 +153,7 @@ export function corpusSelect( sanitize: boolean, spec: ShardSpec | undefined = parseShardSpec(), ): T[] { - corpusCostTable ??= JSON.parse( - readFileSync(new URL("./corpus-costs.json", import.meta.url), "utf8"), - ) as CorpusCosts; + corpusCostTable ??= loadCorpusCosts(); const costs = sanitize ? corpusCostTable.san : corpusCostTable.plain; const known = Object.values(costs).sort((a, b) => a - b); const median = known.length === 0 ? 1 : known[Math.floor(known.length / 2)]!; diff --git a/tests/harness/stdlib-surface.test.ts b/tests/harness/stdlib-surface.test.ts index e91e7be4d..d2ffab6cb 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); } @@ -119,7 +119,7 @@ function calleeSymbol(decl: ts.VariableDeclaration): ts.Symbol | undefined { function isStdlibDeclared(symbol: ts.Symbol | undefined): boolean { if (!symbol) return false; - return checker.declarationsOf(symbol).some((d) => { + return symbol.declarations.some((d) => { const file = d.getSourceFile(); return ( file.fileName === ambient ||